MySQL入门

InnoDB

InnoDB记录存储结构

表中的数据到底存到了哪里?
以什么格式存放的?
MySQL是以什么方式来访问的这些数据?

InnoDB是一个将表中的数据存储到磁盘上的存储引擎。
当我们想从表中获取某些记录时,InnoDB采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。

InnoDB数据页结构

视图

/*
含义:虚拟表,和普通表一样使用
mysql5.1版本出现的新特性,是通过表动态生成的数据

比如:舞蹈班和普通班级的对比
	创建语法的关键字	是否实际占用物理空间	使用

视图	create view		只是保存了sql逻辑	增删改查,只是一般不能增删改

表	create table		保存了数据		增删改查
*/

#案例:查询姓张的学生名和专业名
SELECT stuname,majorname
FROM stuinfo s
INNER JOIN major m ON s.`majorid`= m.`id`
WHERE s.`stuname` LIKE '张%';

CREATE VIEW v1
AS
SELECT stuname,majorname
FROM stuinfo s
INNER JOIN major m ON s.`majorid`= m.`id`;

SELECT * FROM v1 WHERE stuname LIKE '张%';

一、创建视图

/*
语法:
create view 视图名
as
查询语句;
*/
USE myemployees;

#1.查询姓名中包含a字符的员工名、部门名和工种信息
#①创建
CREATE VIEW myv1
AS
SELECT last_name,department_name,job_title
FROM employees e
JOIN departments d ON e.department_id  = d.department_id
JOIN jobs j ON j.job_id  = e.job_id;
#②使用
SELECT * FROM myv1 WHERE last_name LIKE '%a%';

#2.查询各部门的平均工资级别
#①创建视图查看每个部门的平均工资
CREATE VIEW myv2
AS
SELECT AVG(salary) ag,department_id
FROM employees
GROUP BY department_id;
#②使用
SELECT myv2.`ag`,g.grade_level
FROM myv2
JOIN job_grades g
ON myv2.`ag` BETWEEN g.`lowest_sal` AND g.`highest_sal`;

#3.查询平均工资最低的部门信息
SELECT * FROM myv2 ORDER BY ag LIMIT 1;

#4.查询平均工资最低的部门名和工资
CREATE VIEW myv3
AS
SELECT * FROM myv2 ORDER BY ag LIMIT 1;

SELECT d.*,m.ag
FROM myv3 m
JOIN departments d
ON m.`department_id`=d.`department_id`;

二、视图的修改

#方式一:
/*
create or replace view  视图名
as
查询语句;
*/
SELECT * FROM myv3 
CREATE OR REPLACE VIEW myv3
AS
SELECT AVG(salary),job_id
FROM employees
GROUP BY job_id;

#方式二:
/*
语法:
alter view 视图名
as 
查询语句;
*/
ALTER VIEW myv3
AS
SELECT * FROM employees;

三、删除视图

/*
语法:drop view 视图名,视图名,...;
*/
DROP VIEW emp_v1,emp_v2,myv3;

四、查看视图

DESC myv3;

SHOW CREATE VIEW myv3;

五、视图的更新

CREATE OR REPLACE VIEW myv1
AS
SELECT last_name,email,salary*12*(1+IFNULL(commission_pct,0)) "annual salary"
FROM employees;

CREATE OR REPLACE VIEW myv1
AS
SELECT last_name,email
FROM employees;

SELECT * FROM myv1;
SELECT * FROM employees;

#1.插入
INSERT INTO myv1 VALUES('张飞','zf@qq.com');
#2.修改
UPDATE myv1 SET last_name = '张无忌' WHERE last_name='张飞';
#3.删除
DELETE FROM myv1 WHERE last_name = '张无忌';


#具备以下特点的视图不允许更新

#①包含以下关键字的sql语句:分组函数、distinct、group  by、having、union或者union all
CREATE OR REPLACE VIEW myv1
AS
SELECT MAX(salary) m,department_id
FROM employees
GROUP BY department_id;

SELECT * FROM myv1;

#更新
UPDATE myv1 SET m=9000 WHERE department_id=10;

#②常量视图
CREATE OR REPLACE VIEW myv2
AS
SELECT 'john' NAME;

SELECT * FROM myv2;
#更新
UPDATE myv2 SET NAME='lucy';

#③select中包含子查询
CREATE OR REPLACE VIEW myv3
AS
SELECT department_id,(SELECT MAX(salary) FROM employees) 最高工资
FROM departments;

#更新
SELECT * FROM myv3;
UPDATE myv3 SET 最高工资=100000;

#④join
CREATE OR REPLACE VIEW myv4
AS
SELECT last_name,department_name
FROM employees e
JOIN departments d
ON e.department_id  = d.department_id;
#更新
SELECT * FROM myv4;
UPDATE myv4 SET last_name  = '张飞' WHERE last_name='Whalen';
INSERT INTO myv4 VALUES('陈真','xxxx');

#⑤from一个不能更新的视图
CREATE OR REPLACE VIEW myv5
AS
SELECT * FROM myv3;
#更新
SELECT * FROM myv5;
UPDATE myv5 SET 最高工资=10000 WHERE department_id=60;

#⑥where子句的子查询引用了from子句中的表
CREATE OR REPLACE VIEW myv6
AS
SELECT last_name,email,salary
FROM employees
WHERE employee_id IN(
	SELECT  manager_id
	FROM employees
	WHERE manager_id IS NOT NULL
);
#更新
SELECT * FROM myv6;
UPDATE myv6 SET salary=10000 WHERE last_name = 'k_ing';

TCL语言

Transaction Control Language 事务控制语言

/*
事务:一个或一组sql语句组成一个执行单元,这个执行单元要么全部执行,要么全部不执行。

案例:转账
张三丰  1000
郭襄	1000

update 表 set 张三丰的余额=500 where name='张三丰'
意外
update 表 set 郭襄的余额=1500 where name='郭襄'


事务的特性:
ACID
原子性:一个事务不可再分割,要么都执行要么都不执行
一致性:一个事务执行会使数据从一个一致状态切换到另外一个一致状态
隔离性:一个事务的执行不受其他事务的干扰
持久性:一个事务一旦提交,则会永久的改变数据库的数据.

事务的创建
隐式事务:事务没有明显的开启和结束的标记
比如insert、update、delete语句

delete from 表 where id =1;

显式事务:事务具有明显的开启和结束的标记
前提:必须先设置自动提交功能为禁用

set autocommit=0;

步骤1:开启事务
set autocommit=0;
start transaction;可选的

步骤2:编写事务中的sql语句(select insert update delete)
语句1;
语句2;
...

步骤3:结束事务
commit;提交事务
rollback;回滚事务

savepoint 节点名;设置保存点

事务的隔离级别:
		  脏读		不可重复读	幻读
read uncommitted:√		√		√
read committed:  ×		√		√
repeatable read: ×		×		√
serializable	  ×             ×               ×

mysql中默认 第三个隔离级别 repeatable read
oracle中默认第二个隔离级别 read committed
查看隔离级别
select @@tx_isolation;
设置隔离级别
set session|global transaction isolation level 隔离级别;

开启事务的语句;
update 表 set 张三丰的余额=500 where name='张三丰'
update 表 set 郭襄的余额=1500 where name='郭襄' 
结束事务的语句;
*/

SHOW VARIABLES LIKE 'autocommit';
SHOW ENGINES;

1.演示事务的使用步骤

#开启事务
SET autocommit=0;
START TRANSACTION;

#编写一组事务的语句
UPDATE account SET balance = 1000 WHERE username='张无忌';
UPDATE account SET balance = 1000 WHERE username='赵敏';

#结束事务
ROLLBACK;
#commit;

SELECT * FROM account;

2.演示事务对于delete和truncate的处理的区别

SET autocommit=0;
START TRANSACTION;

DELETE FROM account;
ROLLBACK;

3.演示savepoint 的使用

SET autocommit=0;
START TRANSACTION;
DELETE FROM account WHERE id=25;
SAVEPOINT a;#设置保存点
DELETE FROM account WHERE id=28;
ROLLBACK TO a;#回滚到保存点

SELECT * FROM account;

DDL语言

库和表的管理

一、库的管理

1、库的创建
/*
语法:
create database  [if not exists]库名;
*/

#案例:创建库Books
CREATE DATABASE IF NOT EXISTS books ;
2、库的修改
RENAME DATABASE books TO 新库名;

#更改库的字符集
ALTER DATABASE books CHARACTER SET gbk;
3、库的删除
DROP DATABASE IF EXISTS books;

二、表的管理

1.表的创建 ★
/*
语法:
create table 表名(
	列名 列的类型【(长度) 约束】,
	列名 列的类型【(长度) 约束】,
	列名 列的类型【(长度) 约束】,
	...
	列名 列的类型【(长度) 约束】
)
*/

#案例:创建表Book
CREATE TABLE book(
	id INT,#编号
	bName VARCHAR(20),#图书名
	price DOUBLE,#价格
	authorId  INT,#作者编号
	publishDate DATETIME#出版日期
);
DESC book;

#案例:创建表author
CREATE TABLE IF NOT EXISTS author(
	id INT,
	au_name VARCHAR(20),
	nation VARCHAR(10)
)
DESC author;
2.表的修改
/*
语法
alter table 表名 add|drop|modify|change column 列名 【列类型 约束】;
*/

#①修改列名
ALTER TABLE book CHANGE COLUMN publishdate pubDate DATETIME;

#②修改列的类型或约束
ALTER TABLE book MODIFY COLUMN pubdate TIMESTAMP;

#③添加新列
ALTER TABLE author ADD COLUMN annual DOUBLE; 

#④删除列
ALTER TABLE book_author DROP COLUMN  annual;

#⑤修改表名
ALTER TABLE author RENAME TO book_author;

DESC book;
3.表的删除
DROP TABLE IF EXISTS book_author;

SHOW TABLES;

#通用的写法:
DROP DATABASE IF EXISTS 旧库名;
CREATE DATABASE 新库名;

DROP TABLE IF EXISTS 旧表名;
CREATE TABLE  表名();
4.表的复制
INSERT INTO author VALUES
(1,'村上春树','日本'),
(2,'莫言','中国'),
(3,'冯唐','中国'),
(4,'金庸','中国');

SELECT * FROM Author;
SELECT * FROM copy2;
#1.仅仅复制表的结构
CREATE TABLE copy LIKE author;

#2.复制表的结构+数据
CREATE TABLE copy2 
SELECT * FROM author;

#只复制部分数据
CREATE TABLE copy3
SELECT id,au_name
FROM author 
WHERE nation='中国';

#仅仅复制某些字段
CREATE TABLE copy4 
SELECT id,au_name
FROM author
WHERE 0;

常见的数据类型

一、整型

/*
分类:
	tinyint、smallint、mediumint、int/integer、bigint
	1	       2		  3	           4		8

特点:
① 如果不设置无符号还是有符号,默认是有符号,如果想设置无符号,需要添加unsigned关键字
② 如果插入的数值超出了整型的范围,会报out of range异常,并且插入临界值
③ 如果不设置长度,会有默认的长度
长度代表了显示的最大宽度,如果不够会用0在左边填充,但必须搭配zerofill使用!
*/

#1.如何设置无符号和有符号

DROP TABLE IF EXISTS tab_int;
CREATE TABLE tab_int(
	t1 INT(7) ZEROFILL,
	t2 INT(7) ZEROFILL 
);
DESC tab_int;

INSERT INTO tab_int VALUES(-123456);
INSERT INTO tab_int VALUES(-123456,-123456);
INSERT INTO tab_int VALUES(2147483648,4294967296);

INSERT INTO tab_int VALUES(123,123);

SELECT * FROM tab_int;

二、小数

/*
分类:
1.浮点型
float(M,D)
double(M,D)
2.定点型
dec(M,D)
decimal(M,D)

特点:

① M:整数部位+小数部位
  D:小数部位
  如果超过范围,则插入临界值

② M和D都可以省略
  如果是decimal,则M默认为10,D默认为0
  如果是float和double,则会根据插入的数值的精度来决定精度

③ 定点型的精确度较高,如果要求插入数值的精度较高如货币运算等则考虑使用
*/
#测试M和D

DROP TABLE tab_float;
CREATE TABLE tab_float(
	f1 FLOAT,
	f2 DOUBLE,
	f3 DECIMAL
);
SELECT * FROM tab_float;
DESC tab_float;

INSERT INTO tab_float VALUES(123.4523,123.4523,123.4523);
INSERT INTO tab_float VALUES(123.456,123.456,123.456);
INSERT INTO tab_float VALUES(123.4,123.4,123.4);
INSERT INTO tab_float VALUES(1523.4,1523.4,1523.4);

#原则:
/*
所选择的类型越简单越好,能保存数值的类型越小越好
*/

三、字符型

/*
较短的文本:char    varchar
其他:
	binary和varbinary用于保存较短的二进制
	enum用于保存枚举
	set用于保存集合
较长的文本:text    blob(较大的二进制)

特点:
写法		M的意思			特点							空间的耗费			效率
char	char(M)		最大的字符数,可以省略,默认为1		固定长度的字符		比较耗费	高
varchar varchar(M)	最大的字符数,不可以省略			可变长度的字符		比较节省	低
*/


CREATE TABLE tab_char(
	c1 ENUM('a','b','c')
);

INSERT INTO tab_char VALUES('a');
INSERT INTO tab_char VALUES('b');
INSERT INTO tab_char VALUES('c');
INSERT INTO tab_char VALUES('m');
INSERT INTO tab_char VALUES('A');

SELECT * FROM tab_set;

CREATE TABLE tab_set(
	s1 SET('a','b','c','d')
);
INSERT INTO tab_set VALUES('a');
INSERT INTO tab_set VALUES('A,B');
INSERT INTO tab_set VALUES('a,c,d');

四、日期型

/*
分类:
date只保存日期
time 只保存时间
year只保存年

datetime保存日期+时间
timestamp保存日期+时间

特点:

字节		范围		时区等的影响
datetime	 8		1000——9999	                  不受
timestamp	4	    1970-2038	                    受
*/
CREATE TABLE tab_date(
	t1 DATETIME,
	t2 TIMESTAMP
);

INSERT INTO tab_date VALUES(NOW(),NOW());

SELECT * FROM tab_date;

SHOW VARIABLES LIKE 'time_zone';

SET time_zone='+9:00';

常见约束

/*
含义:一种限制,用于限制表中的数据,为了保证表中的数据的准确和可靠性
分类:六大约束
	NOT NULL:非空,用于保证该字段的值不能为空
	比如姓名、学号等
	DEFAULT:默认,用于保证该字段有默认值
	比如性别
	PRIMARY KEY:主键,用于保证该字段的值具有唯一性,并且非空
	比如学号、员工编号等
	UNIQUE:唯一,用于保证该字段的值具有唯一性,可以为空
	比如座位号
	CHECK:检查约束【mysql中不支持】
	比如年龄、性别
	FOREIGN KEY:外键,用于限制两个表的关系,用于保证该字段的值必须来自于主表的关联列的值
		在从表添加外键约束,用于引用主表中某列的值
	比如学生表的专业编号,员工表的部门编号,员工表的工种编号
	
添加约束的时机:
	1.创建表时
	2.修改表时

约束的添加分类:
	列级约束:
		六大约束语法上都支持,但外键约束没有效果
	表级约束:
		除了非空、默认,其他的都支持
		
主键和唯一的大对比:

		保证唯一性  是否允许为空    一个表中可以有多少个   是否允许组合
	主键	√		×		至多有1个           √,但不推荐
	唯一	√		√		可以有多个          √,但不推荐
外键:
	1、要求在从表设置外键关系
	2、从表的外键列的类型和主表的关联列的类型要求一致或兼容,名称无要求
	3、主表的关联列必须是一个key(一般是主键或唯一)
	4、插入数据时,先插入主表,再插入从表
	删除数据时,先删除从表,再删除主表
*/
CREATE TABLE 表名(
	字段名 字段类型 列级约束,
	字段名 字段类型,
	表级约束

)
CREATE DATABASE students;

一、创建表时添加约束

1.添加列级约束
/*
语法:
直接在字段名和类型后面追加 约束类型即可。
只支持:默认、非空、主键、唯一
*/
USE students;
DROP TABLE stuinfo;
CREATE TABLE stuinfo(
	id INT PRIMARY KEY,#主键
	stuName VARCHAR(20) NOT NULL UNIQUE,#非空
	gender CHAR(1) CHECK(gender='男' OR gender ='女'),#检查
	seat INT UNIQUE,#唯一
	age INT DEFAULT  18,#默认约束
	majorId INT REFERENCES major(id)#外键
);
CREATE TABLE major(
	id INT PRIMARY KEY,
	majorName VARCHAR(20)
);
#查看stuinfo中的所有索引,包括主键、外键、唯一
SHOW INDEX FROM stuinfo;
2.添加表级约束
/*
语法:在各个字段的最下面
 【constraint 约束名】 约束类型(字段名) 
*/
DROP TABLE IF EXISTS stuinfo;
CREATE TABLE stuinfo(
	id INT,
	stuname VARCHAR(20),
	gender CHAR(1),
	seat INT,
	age INT,
	majorid INT,
	
	CONSTRAINT pk PRIMARY KEY(id),#主键
	CONSTRAINT uq UNIQUE(seat),#唯一键
	CONSTRAINT ck CHECK(gender ='男' OR gender  = '女'),#检查
	CONSTRAINT fk_stuinfo_major FOREIGN KEY(majorid) REFERENCES major(id)#外键
);
SHOW INDEX FROM stuinfo;

#通用的写法:★
CREATE TABLE IF NOT EXISTS stuinfo(
	id INT PRIMARY KEY,
	stuname VARCHAR(20),
	sex CHAR(1),
	age INT DEFAULT 18,
	seat INT UNIQUE,
	majorid INT,
	CONSTRAINT fk_stuinfo_major FOREIGN KEY(majorid) REFERENCES major(id)

);

二、修改表时添加约束

/*
1、添加列级约束
alter table 表名 modify column 字段名 字段类型 新约束;

2、添加表级约束
alter table 表名 add 【constraint 约束名】 约束类型(字段名) 【外键的引用】;
*/
DROP TABLE IF EXISTS stuinfo;
CREATE TABLE stuinfo(
	id INT,
	stuname VARCHAR(20),
	gender CHAR(1),
	seat INT,
	age INT,
	majorid INT
)
DESC stuinfo;
#1.添加非空约束
ALTER TABLE stuinfo MODIFY COLUMN stuname VARCHAR(20)  NOT NULL;
#2.添加默认约束
ALTER TABLE stuinfo MODIFY COLUMN age INT DEFAULT 18;
#3.添加主键
#①列级约束
ALTER TABLE stuinfo MODIFY COLUMN id INT PRIMARY KEY;
#②表级约束
ALTER TABLE stuinfo ADD PRIMARY KEY(id);

#4.添加唯一
#①列级约束
ALTER TABLE stuinfo MODIFY COLUMN seat INT UNIQUE;
#②表级约束
ALTER TABLE stuinfo ADD UNIQUE(seat);

#5.添加外键
ALTER TABLE stuinfo ADD CONSTRAINT fk_stuinfo_major FOREIGN KEY(majorid) REFERENCES major(id); 

三、修改表时删除约束

#1.删除非空约束
ALTER TABLE stuinfo MODIFY COLUMN stuname VARCHAR(20) NULL;

#2.删除默认约束
ALTER TABLE stuinfo MODIFY COLUMN age INT ;

#3.删除主键
ALTER TABLE stuinfo DROP PRIMARY KEY;

#4.删除唯一
ALTER TABLE stuinfo DROP INDEX seat;

#5.删除外键
ALTER TABLE stuinfo DROP FOREIGN KEY fk_stuinfo_major;

SHOW INDEX FROM stuinfo;

标识列

/*
又称为自增长列
含义:可以不用手动的插入值,系统提供默认的序列值

特点:
1、标识列必须和主键搭配吗?不一定,但要求是一个key
2、一个表可以有几个标识列?至多一个!
3、标识列的类型只能是数值型
4、标识列可以通过 SET auto_increment_increment=3;设置步长
可以通过 手动插入值,设置起始值
*/

#一、创建表时设置标识列
DROP TABLE IF EXISTS tab_identity;
CREATE TABLE tab_identity(
	id INT  ,
	NAME FLOAT UNIQUE AUTO_INCREMENT,
	seat INT
);
TRUNCATE TABLE tab_identity;

INSERT INTO tab_identity(id,NAME) VALUES(NULL,'john');
INSERT INTO tab_identity(NAME) VALUES('lucy');
SELECT * FROM tab_identity;

SHOW VARIABLES LIKE '%auto_increment%';

SET auto_increment_increment=3;

在这里插入图片描述

DML语言

一、插入语句

方式一:经典的插入

/*
语法:
insert into 表名(列名,...) values(值1,...);
*/
SELECT * FROM beauty;

#1.插入的值的类型要与列的类型一致或兼容
INSERT INTO beauty(id,NAME,sex,borndate,phone,photo,boyfriend_id)
VALUES(13,'唐艺昕','女','1990-4-23','1898888888',NULL,2);

#2.不可以为null的列必须插入值。可以为null的列如何插入值?
#方式一:
INSERT INTO beauty(id,NAME,sex,borndate,phone,photo,boyfriend_id)
VALUES(13,'唐艺昕','女','1990-4-23','1898888888',NULL,2);

#方式二:
INSERT INTO beauty(id,NAME,sex,phone)
VALUES(15,'娜扎','女','1388888888');

#3.列的顺序是否可以调换
INSERT INTO beauty(NAME,sex,id,phone)
VALUES('蒋欣','女',16,'110');

#4.列数和值的个数必须一致
INSERT INTO beauty(NAME,sex,id,phone)
VALUES('关晓彤','女',17,'110');

#5.可以省略列名,默认所有列,而且列的顺序和表中列的顺序一致
INSERT INTO beauty
VALUES(18,'张飞','男',NULL,'119',NULL,NULL);

方式二:

/*

语法:
insert into 表名
set 列名=值,列名=值,…
*/

INSERT INTO beauty
SET id=19,NAME=‘刘涛’,phone=‘999’;

#两种方式大pk ★

#1、方式一支持插入多行,方式二不支持

INSERT INTO beauty
VALUES(23,‘唐艺昕1’,‘女’,‘1990-4-23’,‘1898888888’,NULL,2)
,(24,‘唐艺昕2’,‘女’,‘1990-4-23’,‘1898888888’,NULL,2)
,(25,‘唐艺昕3’,‘女’,‘1990-4-23’,‘1898888888’,NULL,2);

#2、方式一支持子查询,方式二不支持

INSERT INTO beauty(id,NAME,phone)
SELECT 26,‘宋茜’,‘11809866’;

INSERT INTO beauty(id,NAME,phone)
SELECT id,boyname,‘1234567’
FROM boys WHERE id<3;

二、修改语句

/*

1.修改单表的记录★

语法:
update 表名
set 列=新值,列=新值,…
where 筛选条件;

2.修改多表的记录【补充】

语法:
sql92语法:
update 表1 别名,表2 别名
set 列=值,…
where 连接条件
and 筛选条件;

sql99语法:
update 表1 别名
inner|left|right join 表2 别名
on 连接条件
set 列=值,…
where 筛选条件;

*/

#1.修改单表的记录
#案例1:修改beauty表中姓唐的女神的电话为13899888899

UPDATE beauty SET phone = ‘13899888899’
WHERE NAME LIKE ‘唐%’;

#案例2:修改boys表中id好为2的名称为张飞,魅力值 10
UPDATE boys SET boyname=‘张飞’,usercp=10
WHERE id=2;

#2.修改多表的记录

#案例 1:修改张无忌的女朋友的手机号为114

UPDATE boys bo
INNER JOIN beauty b ON bo.id=b.boyfriend_id
SET b.phone=‘119’,bo.userCP=1000
WHERE bo.boyName=‘张无忌’;

#案例2:修改没有男朋友的女神的男朋友编号都为2号

UPDATE boys bo
RIGHT JOIN beauty b ON bo.id=b.boyfriend_id
SET b.boyfriend_id=2
WHERE bo.id IS NULL;

SELECT * FROM boys;

三、删除语句

/*

方式一:delete
语法:

1、单表的删除【★】
delete from 表名 where 筛选条件

2、多表的删除【补充】

sql92语法:
delete 表1的别名,表2的别名
from 表1 别名,表2 别名
where 连接条件
and 筛选条件;

sql99语法:

delete 表1的别名,表2的别名
from 表1 别名
inner|left|right join 表2 别名 on 连接条件
where 筛选条件;

方式二:truncate
语法:truncate table 表名;

*/

#方式一:delete
#1.单表的删除
#案例:删除手机号以9结尾的女神信息

DELETE FROM beauty WHERE phone LIKE ‘%9’;
SELECT * FROM beauty;

#2.多表的删除

#案例:删除张无忌的女朋友的信息

DELETE b
FROM beauty b
INNER JOIN boys bo ON b.boyfriend_id = bo.id
WHERE bo.boyName=‘张无忌’;

#案例:删除黄晓明的信息以及他女朋友的信息
DELETE b,bo
FROM beauty b
INNER JOIN boys bo ON b.boyfriend_id=bo.id
WHERE bo.boyName=‘黄晓明’;

#方式二:truncate语句

#案例:将魅力值>100的男神信息删除
TRUNCATE TABLE boys ;

#delete pk truncate【面试题★】

/*

1.delete 可以加where 条件,truncate不能加

2.truncate删除,效率高一丢丢
3.假如要删除的表中有自增长列,
如果用delete删除后,再插入数据,自增长列的值从断点开始,
而truncate删除后,再插入数据,自增长列的值从1开始。
4.truncate删除没有返回值,delete删除有返回值

5.truncate删除不能回滚,delete删除可以回滚.

*/

SELECT * FROM boys;

DELETE FROM boys;
TRUNCATE TABLE boys;
INSERT INTO boys (boyname,usercp)
VALUES(‘张飞’,100),(‘刘备’,100),(‘关云长’,100);

DQL语言

进阶1:基础查询

语法:select 查询列表 from 表名;
特点:
1、查询列表可以是:表中的字段、常量值、表达式、函数
2、查询的结果是一个虚拟的表格

USE myemployees;

1.查询表中的单个字段

SELECT last_name FROM employees;

2.查询表中的多个字段

SELECT last_name,salary,email FROM employees;

3.查询表中的所有字段

#方式一:
SELECT 
    `employee_id`,
    `first_name`,
    `last_name`,
    `phone_number`,
    `last_name`,
    `job_id`,
    `phone_number`,
    `job_id`,
    `salary`,
    `commission_pct`,
    `manager_id`,
    `department_id`,
    `hiredate` 
FROM
    employees ;
#方式二:  
 SELECT * FROM employees;

4.查询常量值

SELECT 100;
SELECT 'john';

5.查询表达式

SELECT 100%98;

6.查询函数

SELECT VERSION();

7.起别名

 /*
	 ①便于理解
	 ②如果要查询的字段有重名的情况,使用别名可以区分开来
 */
 #方式一:使用as
	SELECT 100%98 AS 结果;
	SELECT last_name AS 姓,first_name AS 名 FROM employees;

#方式二:使用空格
	SELECT last_name 姓,first_name 名 FROM employees;

#案例:查询salary,显示结果为 out put
	SELECT salary AS "out put" FROM employees;

8.去重

#案例:查询员工表中涉及到的所有的部门编号
SELECT DISTINCT department_id FROM employees;

9.+号的作用

/*
	java中的+号:
	①运算符,两个操作数都为数值型
	②连接符,只要有一个操作数为字符串
	
	mysql中的+号:
	仅仅只有一个功能:运算符
	
	select 100+90; 两个操作数都为数值型,则做加法运算
	select '123'+90;只要其中一方为字符型,试图将字符型数值转换成数值型
				如果转换成功,则继续做加法运算
	select 'john'+90;	如果转换失败,则将字符型数值转换成0
	select null+10; 只要其中一方为null,则结果肯定为null
*/
#案例:查询员工名和姓连接成一个字段,并显示为 姓名

SELECT CONCAT('a','b','c') AS 结果;

SELECT 
	CONCAT(last_name,first_name) AS 姓名
FROM
	employees;
#第03章_基本的SELECT语句

#1. SQL的分类
/*
DDL:数据定义语言。CREATE \ ALTER \ DROP \ RENAME \ TRUNCATE


DML:数据操作语言。INSERT \ DELETE \ UPDATE \ SELECT (重中之重)


DCL:数据控制语言。COMMIT \ ROLLBACK \ SAVEPOINT \ GRANT \ REVOKE


学习技巧:大处着眼、小处着手。

*/

/*
2.1 SQL的规则 ----必须要遵守
- SQL 可以写在一行或者多行。为了提高可读性,各子句分行写,必要时使用缩进
- 每条命令以 ; 或 \g 或 \G 结束
- 关键字不能被缩写也不能分行
- 关于标点符号
  - 必须保证所有的()、单引号、双引号是成对结束的
  - 必须使用英文状态下的半角输入方式
  - 字符串型和日期时间类型的数据可以使用单引号(' ')表示
  - 列的别名,尽量使用双引号(" "),而且不建议省略as

2.2 SQL的规范  ----建议遵守
- MySQL 在 Windows 环境下是大小写不敏感的
- MySQL 在 Linux 环境下是大小写敏感的
  - 数据库名、表名、表的别名、变量名是严格区分大小写的
  - 关键字、函数名、列名(或字段名)、列的别名(字段的别名) 是忽略大小写的。
- 推荐采用统一的书写规范:
  - 数据库名、表名、表别名、字段名、字段别名等都小写
  - SQL 关键字、函数名、绑定变量等都大写


3. MySQL的三种注释的方式


*/

USE dbtest2;

-- 这是一个查询语句
SELECT * FROM emp;

INSERT INTO emp 
VALUES(1002,'Tom'); #字符串、日期时间类型的变量需要使用一对''表示

INSERT INTO emp 
VALUES(1003,'Jerry');

# SELECT * FROM emp\G

SHOW CREATE TABLE emp\g

/*
4. 导入现有的数据表、表的数据。
方式1:source 文件的全路径名
举例:source d:\atguigudb.sql;


方式2:基于具体的图形化界面的工具可以导入数据
比如:SQLyog中 选择 “工具” -- “执行sql脚本” -- 选中xxx.sql即可。
*/

#5. 最基本的SELECT语句: SELECT 字段1,字段2,... FROM 表名 
SELECT 1 + 1,3 * 2;

SELECT 1 + 1,3 * 2
FROM DUAL; #dual:伪表

# *:表中的所有的字段(或列)
SELECT * FROM employees;

SELECT employee_id,last_name,salary
FROM employees;


#6. 列的别名
# as:全称:alias(别名),可以省略
# 列的别名可以使用一对""引起来,不要使用''。
SELECT employee_id emp_id,last_name AS lname,department_id "部门id",salary * 12 AS "annual sal"
FROM employees;

# 7. 去除重复行
#查询员工表中一共有哪些部门id呢?
#错误的:没有去重的情况
SELECT department_id
FROM employees;
#正确的:去重的情况
SELECT DISTINCT department_id
FROM employees;

#错误的:
SELECT salary,DISTINCT department_id
FROM employees;

#仅仅是没有报错,但是没有实际意义。
SELECT DISTINCT department_id,salary
FROM employees;

#8. 空值参与运算
# 1. 空值:null
# 2. null不等同于0,'','null'
SELECT * FROM employees;

#3. 空值参与运算:结果一定也为空。
SELECT employee_id,salary "月工资",salary * (1 + commission_pct) * 12 "年工资",commission_pct
FROM employees;
#实际问题的解决方案:引入IFNULL
SELECT employee_id,salary "月工资",salary * (1 + IFNULL(commission_pct,0)) * 12 "年工资",commission_pct
FROM `employees`;

#9. 着重号 ``

SELECT * FROM `order`;

#10. 查询常数
SELECT '尚硅谷',123,employee_id,last_name
FROM employees;

#11.显示表结构

DESCRIBE employees; #显示了表中字段的详细信息

DESC employees;

DESC departments;

#12.过滤数据

#练习:查询90号部门的员工信息
SELECT * 
FROM employees
#过滤条件,声明在FROM结构的后面
WHERE department_id = 90;

#练习:查询last_name为'King'的员工信息
SELECT * 
FROM EMPLOYEES
WHERE LAST_NAME = 'King'; 

#第03章_基本的SELECT语句的课后练习

# 1.查询员工12个月的工资总和,并起别名为ANNUAL SALARY
#理解1:计算12月的基本工资
SELECT employee_id,last_name,salary * 12 "ANNUAL SALARY"
FROM employees;

#理解2:计算12月的基本工资和奖金
SELECT employee_id,last_name,salary * 12 * (1 + IFNULL(commission_pct,0)) "ANNUAL SALARY"
FROM employees;


# 2.查询employees表中去除重复的job_id以后的数据
SELECT DISTINCT job_id
FROM employees;

# 3.查询工资大于12000的员工姓名和工资
SELECT last_name,salary
FROM employees
WHERE salary > 12000;

# 4.查询员工号为176的员工的姓名和部门号
SELECT last_name,department_id
FROM employees
WHERE employee_id = 176;

# 5.显示表 departments 的结构,并查询其中的全部数据 
DESCRIBE departments;

SELECT * FROM departments;

进阶2:条件查询

语法:
select 查询列表 from 表名 where 筛选条件;
分类:
一、按条件表达式筛选
简单条件运算符:> < = != <> >= <=
二、按逻辑表达式筛选
逻辑运算符:
作用:用于连接条件表达式
&& || !
and or not
&&和and:两个条件都为true,结果为true,反之为false
||或or: 只要有一个条件为true,结果为true,反之为false
!或not: 如果连接的条件本身为false,结果为true,反之为false
三、模糊查询
like | between and | in | is null

一、按条件表达式筛选

案例1:查询工资>12000的员工信息
SELECT 
	*
FROM
	employees
WHERE
	salary>12000;
案例2:查询部门编号不等于90号的员工名和部门编号
SELECT 
	last_name,
	department_id
FROM
	employees
WHERE
	department_id<>90;

二、按逻辑表达式筛选

案例1:查询工资z在10000到20000之间的员工名、工资以及奖金
SELECT
	last_name,
	salary,
	commission_pct
FROM
	employees
WHERE
	salary>=10000 AND salary<=20000;
案例2:查询部门编号不是在90到110之间,或者工资高于15000的员工信息
SELECT
	*
FROM
	employees
WHERE
	NOT(department_id>=90 AND  department_id<=110) OR salary>15000;

三、模糊查询

like between and in is null|is not null

1.like

特点:
①一般和通配符搭配使用
通配符:
% 任意多个字符,包含0个字符
_ 任意单个字符
*、

#案例1:查询员工名中包含字符a的员工信息
select 
	*
from
	employees
where
	last_name like '%a%';#abc

#案例2:查询员工名中第三个字符为e,第五个字符为a的员工名和工资
select
	last_name,
	salary
FROM
	employees
WHERE
	last_name LIKE '__n_l%';

#案例3:查询员工名中第二个字符为_的员工名
SELECT
	last_name
FROM
	employees
WHERE
	last_name LIKE '_$_%' ESCAPE '$';
2.between and

①使用between and 可以提高语句的简洁度
②包含临界值
③两个临界值不要调换顺序

#案例1:查询员工编号在100到120之间的员工信息
SELECT
	*
FROM
	employees
WHERE
	employee_id >= 120 AND employee_id<=100;
	
#----------------------

SELECT
	*
FROM
	employees
WHERE
	employee_id BETWEEN 120 AND 100;
3.in

含义:判断某字段的值是否属于in列表中的某一项
特点:
①使用in提高语句简洁度
②in列表的值类型必须一致或兼容
③in列表中不支持通配符

#案例:查询员工的工种编号是 IT_PROG、AD_VP、AD_PRES中的一个员工名和工种编号
SELECT
	last_name,
	job_id
FROM
	employees
WHERE
	job_id = 'IT_PROT' OR job_id = 'AD_VP' OR JOB_ID ='AD_PRES';

#------------------

SELECT
	last_name,
	job_id
FROM
	employees
WHERE
	job_id IN( 'IT_PROT' ,'AD_VP','AD_PRES');
4、is null

=或<>不能用于判断null值
is null或is not null 可以判断null值

#案例1:查询有奖金的员工名和奖金率
SELECT
	last_name,
	commission_pct
FROM
	employees
WHERE
	commission_pct IS NOT NULL;

#----------以下为×

SELECT
	last_name,
	commission_pct
FROM
	employees
WHERE 
	salary IS 12000;
安全等于 <=>
#案例1:查询没有奖金的员工名和奖金率
SELECT
	last_name,
	commission_pct
FROM
	employees
WHERE
	commission_pct <=>NULL;
	
#案例2:查询工资为12000的员工信息
SELECT
	last_name,
	salary
FROM
	employees
WHERE 
	salary <=> 12000;
is null pk <=>

IS NULL:仅仅可以判断NULL值,可读性较高,建议使用
<=> :既可以判断NULL值,又可以判断普通的数值,可读性较低

# 第04章_运算符
#1. 算术运算符: +  -  *  /  div  % mod

SELECT 100, 100 + 0, 100 - 0, 100 + 50, 100 + 50 * 30, 100 + 35.5, 100 - 35.5 
FROM DUAL;

# 在SQL中,+没有连接的作用,就表示加法运算。此时,会将字符串转换为数值(隐式转换)
SELECT 100 + '1'  # 在Java语言中,结果是:1001。 
FROM DUAL;

SELECT 100 + 'a' #此时将'a'看做0处理
FROM DUAL;

SELECT 100 + NULL  # null值参与运算,结果为null
FROM DUAL;

SELECT 100, 100 * 1, 100 * 1.0, 100 / 1.0, 100 / 2,
100 + 2 * 5 / 2,100 / 3, 100 DIV 0  # 分母如果为0,则结果为null
FROM DUAL;

# 取模运算: % mod
SELECT 12 % 3,12 % 5, 12 MOD -5,-12 % 5,-12 % -5
FROM DUAL;

#练习:查询员工id为偶数的员工信息
SELECT employee_id,last_name,salary
FROM employees
WHERE employee_id % 2 = 0;

#2. 比较运算符
#2.1 =  <=>  <> !=  <  <=  >  >= 

# = 的使用
SELECT 1 = 2,1 != 2,1 = '1',1 = 'a',0 = 'a' #字符串存在隐式转换。如果转换数值不成功,则看做0
FROM DUAL;

SELECT 'a' = 'a','ab' = 'ab','a' = 'b' #两边都是字符串的话,则按照ANSI的比较规则进行比较。
FROM DUAL;

SELECT 1 = NULL,NULL = NULL # 只要有null参与判断,结果就为null
FROM DUAL;

SELECT last_name,salary,commission_pct
FROM employees
#where salary = 6000;
WHERE commission_pct = NULL;  #此时执行,不会有任何的结果

# <=> :安全等于。 记忆技巧:为NULL而生。

SELECT 1 <=> 2,1 <=> '1',1 <=> 'a',0 <=> 'a'
FROM DUAL;

SELECT 1 <=> NULL, NULL <=> NULL
FROM DUAL;

#练习:查询表中commission_pct为null的数据有哪些
SELECT last_name,salary,commission_pct
FROM employees
WHERE commission_pct <=> NULL;

SELECT 3 <> 2,'4' <> NULL, '' != NULL,NULL != NULL
FROM DUAL;

#2.2 
#① IS NULL \ IS NOT NULL \ ISNULL
#练习:查询表中commission_pct为null的数据有哪些
SELECT last_name,salary,commission_pct
FROM employees
WHERE commission_pct IS NULL;
#或
SELECT last_name,salary,commission_pct
FROM employees
WHERE ISNULL(commission_pct);

#练习:查询表中commission_pct不为null的数据有哪些
SELECT last_name,salary,commission_pct
FROM employees
WHERE commission_pct IS NOT NULL;
#或
SELECT last_name,salary,commission_pct
FROM employees
WHERE NOT commission_pct <=> NULL;

#② LEAST() \ GREATEST 

SELECT LEAST('g','b','t','m'),GREATEST('g','b','t','m')
FROM DUAL;

SELECT LEAST(first_name,last_name),LEAST(LENGTH(first_name),LENGTH(last_name))
FROM employees;

#③ BETWEEN 条件下界1 AND 条件上界2  (查询条件1和条件2范围内的数据,包含边界)
#查询工资在6000 到 8000的员工信息
SELECT employee_id,last_name,salary
FROM employees
#where salary between 6000 and 8000;
WHERE salary >= 6000 && salary <= 8000;

#交换6000 和 8000之后,查询不到数据
SELECT employee_id,last_name,salary
FROM employees
WHERE salary BETWEEN 8000 AND 6000;

#查询工资不在6000 到 8000的员工信息
SELECT employee_id,last_name,salary
FROM employees
WHERE salary NOT BETWEEN 6000 AND 8000;
#where salary < 6000 or salary > 8000;

#④ in (set)\ not in (set)

#练习:查询部门为10,20,30部门的员工信息
SELECT last_name,salary,department_id
FROM employees
#where department_id = 10 or department_id = 20 or department_id = 30;
WHERE department_id IN (10,20,30);

#练习:查询工资不是6000,7000,8000的员工信息
SELECT last_name,salary,department_id
FROM employees
WHERE salary NOT IN (6000,7000,8000);

#⑤ LIKE :模糊查询
# % : 代表不确定个数的字符 (0个,1个,或多个)

#练习:查询last_name中包含字符'a'的员工信息
SELECT last_name
FROM employees
WHERE last_name LIKE '%a%';

#练习:查询last_name中以字符'a'开头的员工信息
SELECT last_name
FROM employees
WHERE last_name LIKE 'a%';

#练习:查询last_name中包含字符'a'且包含字符'e'的员工信息
#写法1:
SELECT last_name
FROM employees
WHERE last_name LIKE '%a%' AND last_name LIKE '%e%';
#写法2:
SELECT last_name
FROM employees
WHERE last_name LIKE '%a%e%' OR last_name LIKE '%e%a%';

# _ :代表一个不确定的字符

#练习:查询第3个字符是'a'的员工信息
SELECT last_name
FROM employees
WHERE last_name LIKE '__a%';

#练习:查询第2个字符是_且第3个字符是'a'的员工信息
#需要使用转义字符: \ 
SELECT last_name
FROM employees
WHERE last_name LIKE '_\_a%';

#或者  (了解)
SELECT last_name
FROM employees
WHERE last_name LIKE '_$_a%' ESCAPE '$';

#⑥ REGEXP \ RLIKE :正则表达式

SELECT 'shkstart' REGEXP '^shk', 'shkstart' REGEXP 't$', 'shkstart' REGEXP 'hk'
FROM DUAL;

SELECT 'atguigu' REGEXP 'gu.gu','atguigu' REGEXP '[ab]'
FROM DUAL;

#3. 逻辑运算符: OR ||  AND && NOT ! XOR

# or  and 
SELECT last_name,salary,department_id
FROM employees
#where department_id = 10 or department_id = 20;
#where department_id = 10 and department_id = 20;
WHERE department_id = 50 AND salary > 6000;

# not 
SELECT last_name,salary,department_id
FROM employees
#where salary not between 6000 and 8000;
#where commission_pct is not null;
WHERE NOT commission_pct <=> NULL;

# XOR :追求的"异"
SELECT last_name,salary,department_id
FROM employees
WHERE department_id = 50 XOR salary > 6000;

#注意:AND的优先级高于OR

#4. 位运算符: & |  ^  ~  >>   <<

SELECT 12 & 5, 12 | 5,12 ^ 5 
FROM DUAL;

SELECT 10 & ~1 FROM DUAL;

#在一定范围内满足:每向左移动1位,相当于乘以2;每向右移动一位,相当于除以2。
SELECT 4 << 1 , 8 >> 1
FROM DUAL;

进阶3:排序查询

语法:
select
	要查询的东西
from
	表
where 
	条件

order by 排序的字段|表达式|函数|别名 【asc|desc】

特点:
1、asc代表的是升序,可以省略;desc代表的是降序
2、order by子句可以支持 单个字段、别名、表达式、函数、多个字段
3、order by子句在查询语句的最后面,除了limit子句

1、按单个字段排序

SELECT * FROM employees ORDER BY salary DESC;

2、添加筛选条件再排序

#案例:查询部门编号>=90的员工信息,并按员工编号降序
SELECT *
FROM employees
WHERE department_id>=90
ORDER BY employee_id DESC;

3、按表达式排序

#案例:查询员工信息 按年薪降序
SELECT *,salary*12*(1+IFNULL(commission_pct,0))
FROM employees
ORDER BY salary*12*(1+IFNULL(commission_pct,0)) DESC;

4、按别名排序

#案例:查询员工信息 按年薪升序

SELECT *,salary*12*(1+IFNULL(commission_pct,0)) 年薪
FROM employees
ORDER BY 年薪 ASC;

5、按函数排序

#案例:查询员工名,并且按名字的长度降序

SELECT LENGTH(last_name),last_name 
FROM employees
ORDER BY LENGTH(last_name) DESC;

6、按多个字段排序

#案例:查询员工信息,要求先按工资降序,再按employee_id升序
SELECT *
FROM employees
ORDER BY salary DESC,employee_id ASC;

进阶4:常见函数

​ 一、单行函数
​ 1、字符函数
​ concat拼接
​ substr截取子串
​ upper转换成大写
​ lower转换成小写
​ trim去前后指定的空格和字符
​ ltrim去左边空格
​ rtrim去右边空格
​ replace替换
​ lpad左填充
​ rpad右填充
​ instr返回子串第一次出现的索引
​ length 获取字节个数

​ 2、数学函数
​ round 四舍五入
​ rand 随机数
​ floor向下取整
​ ceil向上取整
​ mod取余
​ truncate截断
​ 3、日期函数
​ now当前系统日期+时间
​ curdate当前系统日期
​ curtime当前系统时间
​ str_to_date 将字符转换成日期
​ date_format将日期转换成字符
​ 4、流程控制函数
​ if 处理双分支
​ case语句 处理多分支
​ 情况1:处理等值判断
​ 情况2:处理条件判断

​ 5、其他函数
​ version版本
​ database当前库
​ user当前连接用户
二、分组函数
sum 求和
max 最大值
min 最小值
avg 平均值
count 计数

	特点:
	1、以上五个分组函数都忽略null值,除了count(*)
	2、sum和avg一般用于处理数值型
		max、min、count可以处理任何数据类型
    3、都可以搭配distinct使用,用于统计去重后的结果
	4、count的参数可以支持:
		字段、*、常量值,一般放1

	   建议使用 count(*)

一、字符函数

1.length 获取参数值的字节个数

SELECT LENGTH(‘john’);
SELECT LENGTH(‘张三丰hahaha’);

SHOW VARIABLES LIKE ‘%char%’

2.concat 拼接字符串

SELECT CONCAT(last_name,‘_’,first_name) 姓名 FROM employees;

3.upper、lower

SELECT UPPER(‘john’);
SELECT LOWER(‘joHn’);
#示例:将姓变大写,名变小写,然后拼接
SELECT CONCAT(UPPER(last_name),LOWER(first_name)) 姓名 FROM employees;

4.substr、substring
注意:索引从1开始
#截取从指定索引处后面所有字符
SELECT SUBSTR('李莫愁爱上了陆展元',7)  out_put;

#截取从指定索引处指定字符长度的字符
SELECT SUBSTR('李莫愁爱上了陆展元',1,3) out_put;

#案例:姓名中首字符大写,其他字符小写然后用_拼接,显示出来

SELECT CONCAT(UPPER(SUBSTR(last_name,1,1)),'_',LOWER(SUBSTR(last_name,2)))  out_put
FROM employees;
5.instr 返回子串第一次出现的索引,如果找不到返回0

SELECT INSTR('杨不殷六侠悔爱上了殷六侠','殷八侠') AS out_put;

6.trim
SELECT LENGTH(TRIM('    张翠山    ')) AS out_put;

SELECT TRIM('aa' FROM 'aaaaaaaaa张aaaaaaaaaaaa翠山aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')  AS out_put;
7.lpad 用指定的字符实现左填充指定长度

SELECT LPAD('殷素素',2,'*') AS out_put;

8.rpad 用指定的字符实现右填充指定长度

SELECT RPAD('殷素素',12,'ab') AS out_put;

9.replace 替换

SELECT REPLACE('周芷若周芷若周芷若周芷若张无忌爱上了周芷若','周芷若','赵敏') AS out_put;

二、数学函数

round 四舍五入
SELECT ROUND(-1.55);
SELECT ROUND(1.567,2);
ceil 向上取整,返回>=该参数的最小整数

SELECT CEIL(-1.02);

floor 向下取整,返回<=该参数的最大整数

SELECT FLOOR(-9.99);

truncate 截断

SELECT TRUNCATE(1.69999,1);

mod取余
/*
	mod(a,b) :  a-a/b*b
	mod(-10,-3):-10- (-10)/(-3)*(-3)=-1
*/
SELECT MOD(10,-3);
SELECT 10%3;

三、日期函数

now 返回当前系统日期+时间

SELECT NOW();

curdate 返回当前系统日期,不包含时间

SELECT CURDATE();

curtime 返回当前时间,不包含日期

SELECT CURTIME();

可以获取指定的部分,年、月、日、小时、分钟、秒
SELECT YEAR(NOW()) 年;
SELECT YEAR('1998-1-1') 年;
SELECT YEAR(hiredate) 年 FROM employees;
SELECT MONTH(NOW()) 月;
SELECT MONTHNAME(NOW()) 月;
str_to_date 将字符通过指定的格式转换成日期
SELECT STR_TO_DATE('1998-3-2','%Y-%c-%d') AS out_put;

#查询入职日期为1992--4-3的员工信息
SELECT * FROM employees WHERE hiredate = '1992-4-3';

SELECT * FROM employees WHERE hiredate = STR_TO_DATE('4-3 1992','%c-%d %Y');
date_format 将日期转换成字符
SELECT DATE_FORMAT(NOW(),'%y年%m月%d日') AS out_put;

#查询有奖金的员工名和入职日期(xx月/xx日 xx年)
SELECT last_name,DATE_FORMAT(hiredate,'%m月/%d日 %y年') 入职日期
FROM employees
WHERE commission_pct IS NOT NULL;

四、其他函数

SELECT VERSION();
SELECT DATABASE();
SELECT USER();

五、流程控制函数

1.if函数: if else 的效果
SELECT IF(10<5,'大','小');

SELECT last_name,commission_pct,IF(commission_pct IS NULL,'没奖金,呵呵','有奖金,嘻嘻') 备注
FROM employees;
2.case函数的使用一: switch case 的效果
/*
java中
	switch(变量或表达式){
		case 常量1:语句1;break;
		...
		default:语句n;break;
	
	
	}

mysql中
	case 要判断的字段或表达式
	when 常量1 then 要显示的值1或语句1;
	when 常量2 then 要显示的值2或语句2;
	...
	else 要显示的值n或语句n;
	end
*/

/*
案例:查询员工的工资,要求
	部门号=30,显示的工资为1.1倍
	部门号=40,显示的工资为1.2倍
	部门号=50,显示的工资为1.3倍
	其他部门,显示的工资为原工资
*/
SELECT salary 原始工资,department_id,
CASE department_id
WHEN 30 THEN salary*1.1
WHEN 40 THEN salary*1.2
WHEN 50 THEN salary*1.3
ELSE salary
END AS 新工资
FROM employees;
3.case 函数的使用二:类似于 多重if
/*
java中:
	if(条件1){
		语句1;
	}else if(条件2){
		语句2;
	}
	...
	else{
		语句n;
	}

mysql中:
	case 
	when 条件1 then 要显示的值1或语句1
	when 条件2 then 要显示的值2或语句2
	。。。
	else 要显示的值n或语句n
	end
*/

#案例:查询员工的工资的情况
	如果工资>20000,显示A级别
	如果工资>15000,显示B级别
	如果工资>10000,显示C级别
	否则,显示D级别

SELECT salary,
CASE 
WHEN salary>20000 THEN 'A'
WHEN salary>15000 THEN 'B'
WHEN salary>10000 THEN 'C'
ELSE 'D'
END AS 工资级别
FROM employees;

六、分组函数

1、简单的使用
SELECT SUM(salary) FROM employees;
SELECT AVG(salary) FROM employees;
SELECT MIN(salary) FROM employees;
SELECT MAX(salary) FROM employees;
SELECT COUNT(salary) FROM employees;


SELECT SUM(salary) 和,AVG(salary) 平均,MAX(salary) 最高,MIN(salary) 最低,COUNT(salary) 个数
FROM employees;

SELECT SUM(salary) 和,ROUND(AVG(salary),2) 平均,MAX(salary) 最高,MIN(salary) 最低,COUNT(salary) 个数
FROM employees;
2、参数支持哪些类型
#只适合数字
SELECT SUM(last_name) ,AVG(last_name) FROM employees;
SELECT SUM(hiredate) ,AVG(hiredate) FROM employees;
#可以字符、日期
SELECT MAX(last_name),MIN(last_name) FROM employees;
SELECT MAX(hiredate),MIN(hiredate) FROM employees;
#所有类型
SELECT COUNT(commission_pct) FROM employees;
SELECT COUNT(last_name) FROM employees;
3、是否忽略null
#忽略
SELECT SUM(commission_pct) ,AVG(commission_pct),SUM(commission_pct)/35,SUM(commission_pct)/107 FROM employees;

SELECT MAX(commission_pct) ,MIN(commission_pct) FROM employees;

SELECT COUNT(commission_pct) FROM employees;
SELECT commission_pct FROM employees;
4、和distinct搭配
SELECT SUM(DISTINCT salary),SUM(salary) FROM employees;

SELECT COUNT(DISTINCT salary),COUNT(salary) FROM employees;
5、count函数的详细介绍
SELECT COUNT(salary) FROM employees;

SELECT COUNT(*) FROM employees;

SELECT COUNT(1) FROM employees;

效率:
MYISAM存储引擎下,COUNT(*)的效率高
INNODB存储引擎下,COUNT(*)和COUNT(1)的效率差不多,比COUNT(字段)要高一些
6、和分组函数一同查询的字段有限制

SELECT AVG(salary),employee_id FROM employees;

进阶5:分组查询

语法:
select 查询的字段,分组函数
from 表
group by 分组的字段


​ 特点:
​ 1、可以按单个字段分组
​ 2、和分组函数一同查询的字段最好是分组后的字段
​ 3、分组筛选
​ 针对的表 位置 关键字
​ 分组前筛选: 原始表 group by的前面 where
​ 分组后筛选: 分组后的结果集 group by的后面 having

​ 4、可以按多个字段分组,字段之间用逗号隔开
​ 5、可以支持排序
​ 6、having后可以支持别名
/*
语法:

select 查询列表
from 表
【where 筛选条件】
group by 分组的字段
【order by 排序的字段】;

特点:
1、和分组函数一同查询的字段必须是group by后出现的字段
2、筛选分为两类:分组前筛选和分组后筛选
针对的表 位置 连接的关键字
分组前筛选 原始表 group by前 where

分组后筛选 group by后的结果集 group by后 having

问题1:分组函数做筛选能不能放在where后面
答:不能

问题2:where——group by——having

一般来讲,能用分组前筛选的,尽量使用分组前筛选,提高效率

3、分组可以按单个字段也可以按多个字段
4、可以搭配着排序使用

*/

#引入:查询每个部门的员工个数

SELECT COUNT(*) FROM employees WHERE department_id=90;

一、简单的分组

#案例1:查询每个工种的员工平均工资
SELECT AVG(salary),job_id
FROM employees
GROUP BY job_id;

#案例2:查询每个位置的部门个数
SELECT COUNT(*),location_id
FROM departments
GROUP BY location_id;

二、可以实现分组前的筛选

#案例1:查询邮箱中包含a字符的 每个部门的最高工资
	SELECT MAX(salary),department_id
	FROM employees
	WHERE email LIKE '%a%'
	GROUP BY department_id;

#案例2:查询有奖金的每个领导手下员工的平均工资
	SELECT AVG(salary),manager_id
	FROM employees
	WHERE commission_pct IS NOT NULL
	GROUP BY manager_id;

三、分组后筛选

#案例1:查询哪个部门的员工个数>5
#①查询每个部门的员工个数
	SELECT COUNT(*),department_id
	FROM employees
	GROUP BY department_id;

#② 筛选刚才①结果
	SELECT COUNT(*),department_id
	FROM employees
	GROUP BY department_id
	HAVING COUNT(*)>5;

#案例2:每个工种有奖金的员工的最高工资>12000的工种编号和最高工资
	SELECT job_id,MAX(salary)
	FROM employees
	WHERE commission_pct IS NOT NULL
	GROUP BY job_id
	HAVING MAX(salary)>12000;


#案例3:领导编号>102的每个领导手下的最低工资大于5000的领导编号和最低工资
	SELECT manager_id,MIN(salary)
	FROM employees
	WHERE manager_id>102
	GROUP BY manager_id
	HAVING MIN(salary)>5000;

四、添加排序

#案例:每个工种有奖金的员工的最高工资>6000的工种编号和最高工资,按最高工资升序
	SELECT job_id,MAX(salary) m
	FROM employees
	WHERE commission_pct IS NOT NULL
	GROUP BY job_id
	HAVING m>6000
	ORDER BY m ;

五、按多个字段分组

#案例:查询每个工种每个部门的最低工资,并按最低工资降序
	SELECT MIN(salary),job_id,department_id
	FROM employees
	GROUP BY department_id,job_id
	ORDER BY MIN(salary) DESC;

进阶6:多表连接查询

笛卡尔乘积:如果连接条件省略或无效则会出现
解决办法:添加上连接条件

一、传统模式下的连接 :等值连接——非等值连接

1.等值连接的结果 = 多个表的交集
2.n表连接,至少需要n-1个连接条件
3.多个表不分主次,没有顺序要求
4.一般为表起别名,提高阅读性和性能

二、sql99语法:通过join关键字实现连接

含义:1999年推出的sql语法
支持:
等值连接、非等值连接 (内连接)
外连接
交叉连接

语法:

select 字段,...
from 表1
【inner|left outer|right outer|cross】join 表2 on  连接条件
【inner|left outer|right outer|cross】join 表3 on  连接条件
【where 筛选条件】
【group by 分组字段】
【having 分组后的筛选条件】
【order by 排序的字段或表达式】

好处:语句上,连接条件和筛选条件实现了分离,简洁明了!


三、自连接

案例:查询员工名和直接上级的名称

sql99

SELECT e.last_name,m.last_name
FROM employees e
JOIN employees m ON e.`manager_id`=m.`employee_id`;

sql92

SELECT e.last_name,m.last_name
FROM employees e,employees m 
WHERE e.`manager_id`=m.`employee_id`;

/*
含义:又称多表查询,当查询的字段来自于多个表时,就会用到连接查询

笛卡尔乘积现象:表1 有m行,表2有n行,结果=m*n行

发生原因:没有有效的连接条件
如何避免:添加有效的连接条件

分类:

按年代分类:
sql92标准:仅仅支持内连接
sql99标准【推荐】:支持内连接+外连接(左外和右外)+交叉连接

按功能分类:
	内连接:
		等值连接
		非等值连接
		自连接
	外连接:
		左外连接
		右外连接
		全外连接
	
	交叉连接

*/

SELECT * FROM beauty;

SELECT * FROM boys;

SELECT NAME,boyName FROM boys,beauty
WHERE beauty.boyfriend_id= boys.id;

一、sql92标准

1.等值连接

① 多表等值连接的结果为多表的交集部分
② n表连接,至少需要n-1个连接条件
③ 多表的顺序没有要求
④ 一般需要为表起别名
⑤ 可以搭配前面介绍的所有子句使用,比如排序、分组、筛选

#案例1:查询女神名和对应的男神名
	SELECT NAME,boyName 
	FROM boys,beauty
	WHERE beauty.boyfriend_id= boys.id;

#案例2:查询员工名和对应的部门名
	SELECT last_name,department_name
	FROM employees,departments
	WHERE employees.`department_id`=departments.`department_id`;
为表起别名

① 提高语句的简洁度
② 区分多个重名的字段
注意:如果为表起了别名,则查询的字段就不能使用原来的表名去限定

#查询员工名、工种号、工种名
	SELECT e.last_name,e.job_id,j.job_title
	FROM employees  e,jobs j
	WHERE e.`job_id`=j.`job_id`;
两个表的顺序是否可以调换
#查询员工名、工种号、工种名
	SELECT e.last_name,e.job_id,j.job_title
	FROM jobs j,employees e
	WHERE e.`job_id`=j.`job_id`;
可以加筛选
#案例:查询有奖金的员工名、部门名
	SELECT last_name,department_name,commission_pct
	FROM employees e,departments d
	WHERE e.`department_id`=d.`department_id`
	AND e.`commission_pct` IS NOT NULL;

#案例2:查询城市名中第二个字符为o的部门名和城市名
	SELECT department_name,city
	FROM departments d,locations l
	WHERE d.`location_id` = l.`location_id`
	AND city LIKE '_o%';
可以加分组
#案例1:查询每个城市的部门个数
	SELECT COUNT(*) 个数,city
	FROM departments d,locations l
	WHERE d.`location_id`=l.`location_id`
	GROUP BY city;

#案例2:查询有奖金的每个部门的部门名和部门的领导编号和该部门的最低工资
	SELECT department_name,d.`manager_id`,MIN(salary)
	FROM departments d,employees e
	WHERE d.`department_id`=e.`department_id`
	AND commission_pct IS NOT NULL
	GROUP BY department_name,d.`manager_id`;
可以加排序
#案例:查询每个工种的工种名和员工的个数,并且按员工个数降序
	SELECT job_title,COUNT(*)
	FROM employees e,jobs j
	WHERE e.`job_id`=j.`job_id`
	GROUP BY job_title
	ORDER BY COUNT(*) DESC;
可以实现三表连接?
#案例:查询员工名、部门名和所在的城市
	SELECT last_name,department_name,city
	FROM employees e,departments d,locations l
	WHERE e.`department_id`=d.`department_id`
	AND d.`location_id`=l.`location_id`
	AND city LIKE 's%'
	ORDER BY department_name DESC;

2.非等值连接

#案例1:查询员工的工资和工资级别
	SELECT salary,grade_level
	FROM employees e,job_grades g
	WHERE salary BETWEEN g.`lowest_sal` AND g.`highest_sal`
	AND g.`grade_level`='A';

3.自连接

#案例:查询员工名和上级的名称
	SELECT e.employee_id,e.last_name,m.employee_id,m.last_name
	FROM employees e,employees m
	WHERE e.`manager_id`=m.`employee_id`;

二、sql99语法

/*
语法:
select 查询列表
from 表1 别名 【连接类型】
join 表2 别名
on 连接条件
【where 筛选条件】
【group by 分组】
【having 筛选条件】
【order by 排序列表】

分类:
内连接(★):inner
外连接
左外(★):left 【outer】
右外(★):right 【outer】
全外:full【outer】
交叉连接:cross

*/

1.内连接

/*
语法:

select 查询列表
from 表1 别名
inner join 表2 别名
on 连接条件;

分类:
等值
非等值
自连接

特点:
①添加排序、分组、筛选
②inner可以省略
③ 筛选条件放在where后面,连接条件放在on后面,提高分离性,便于阅读
④inner join连接和sql92语法中的等值连接效果是一样的,都是查询多表的交集

*/

等值连接
#案例1.查询员工名、部门名
	SELECT last_name,department_name
	FROM departments d
	JOIN  employees e
	ON e.`department_id` = d.`department_id`;

#案例2.查询名字中包含e的员工名和工种名(添加筛选)
	SELECT last_name,job_title
	FROM employees e
	INNER JOIN jobs j
	ON e.`job_id`=  j.`job_id`
	WHERE e.`last_name` LIKE '%e%';

#案例3. 查询部门个数>3的城市名和部门个数,(添加分组+筛选)
	①查询每个城市的部门个数
	②在①结果上筛选满足条件的
	SELECT city,COUNT(*) 部门个数
	FROM departments d
	INNER JOIN locations l
	ON d.`location_id`=l.`location_id`
	GROUP BY city
	HAVING COUNT(*)>3;

#案例4.查询哪个部门的员工个数>3的部门名和员工个数,并按个数降序(添加排序)
	#①查询每个部门的员工个数
	SELECT COUNT(*),department_name
	FROM employees e
	INNER JOIN departments d
	ON e.`department_id`=d.`department_id`
	GROUP BY department_name
	#② 在①结果上筛选员工个数>3的记录,并排序
	SELECT COUNT(*) 个数,department_name
	FROM employees e
	INNER JOIN departments d
	ON e.`department_id`=d.`department_id`
	GROUP BY department_name
	HAVING COUNT(*)>3
	ORDER BY COUNT(*) DESC;

#案例5.查询员工名、部门名、工种名,并按部门名降序(添加三表连接)
	SELECT last_name,department_name,job_title
	FROM employees e
	INNER JOIN departments d ON e.`department_id`=d.`department_id`
	INNER JOIN jobs j ON e.`job_id` = j.`job_id`
	ORDER BY department_name DESC;
非等值连接
#查询员工的工资级别
	SELECT salary,grade_level
	FROM employees e
	JOIN job_grades g
	ON e.`salary` BETWEEN g.`lowest_sal` AND g.`highest_sal`;

#查询工资级别的个数>20的个数,并且按工资级别降序
	SELECT COUNT(*),grade_level
	FROM employees e
	JOIN job_grades g
	ON e.`salary` BETWEEN g.`lowest_sal` AND g.`highest_sal`
	GROUP BY grade_level
	HAVING COUNT(*)>20
	ORDER BY grade_level DESC;
自连接
#查询员工的名字、上级的名字
	SELECT e.last_name,m.last_name
	FROM employees e
	JOIN employees m
	ON e.`manager_id`= m.`employee_id`;

#查询姓名中包含字符k的员工的名字、上级的名字
	SELECT e.last_name,m.last_name
	FROM employees e
	JOIN employees m
	ON e.`manager_id`= m.`employee_id`
	WHERE e.`last_name` LIKE '%k%';

2.外连接

应用场景:用于查询一个表中有,另一个表没有的记录
特点:
1、外连接的查询结果为主表中的所有记录
如果从表中有和它匹配的,则显示匹配的值
如果从表中没有和它匹配的,则显示null
外连接查询结果=内连接结果+主表中有而从表没有的记录
2、左外连接,left join左边的是主表
右外连接,right join右边的是主表
3、左外和右外交换两个表的顺序,可以实现同样的效果
4、全外连接=内连接的结果+表1中有但表2没有的+表2中有但表1没有的

#引入:查询男朋友不在男神表的的女神名
SELECT * FROM beauty;
SELECT * FROM boys;
左外连接
	SELECT b.*,bo.*
	FROM boys bo
	LEFT OUTER JOIN beauty b
	ON b.`boyfriend_id` = bo.`id`
	WHERE b.`id` IS NULL;
 
#案例1:查询哪个部门没有员工
#左外
	SELECT d.*,e.employee_id
	FROM departments d
	LEFT OUTER JOIN employees e
	ON d.`department_id` = e.`department_id`
	WHERE e.`employee_id` IS NULL;
右外连接
	SELECT d.*,e.employee_id
	FROM employees e
	RIGHT OUTER JOIN departments d
	ON d.`department_id` = e.`department_id`
	WHERE e.`employee_id` IS NULL;
全外连接
	USE girls;
	SELECT b.*,bo.*
	FROM beauty b
	FULL OUTER JOIN boys bo
	ON b.`boyfriend_id` = bo.id;

3.交叉连接

	SELECT b.*,bo.*
	FROM beauty b
	CROSS JOIN boys bo;

三、sql92和sql99pk

功能:sql99支持的较多
可读性:sql99实现连接条件和筛选条件的分离,可读性较高

进阶7:子查询

含义:

一条查询语句中又嵌套了另一条完整的select语句,其中被嵌套的select语句,称为子查询或内查询
在外面的查询语句,称为主查询或外查询

特点:

1、子查询都放在小括号内
2、子查询可以放在from后面、select后面、where后面、having后面,但一般放在条件的右侧
3、子查询优先于主查询执行,主查询使用了子查询的执行结果
4、子查询根据查询结果的行数不同分为以下两类:
① 单行子查询
	结果集只有一行
	一般搭配单行操作符使用:> < = <> >= <= 
	非法使用子查询的情况:
	a、子查询的结果为一组值
	b、子查询的结果为空
	
② 多行子查询
	结果集有多行
	一般搭配多行操作符使用:any、all、in、not in
	in: 属于子查询结果中的任意一个就行
	any和all往往可以用其他查询代替

/*
含义:
出现在其他语句中的select语句,称为子查询或内查询
外部的查询语句,称为主查询或外查询

分类:
按子查询出现的位置:
select后面:
仅仅支持标量子查询

from后面:
	支持表子查询
where或having后面:★
	标量子查询(单行) √
	列子查询  (多行) √
	
	行子查询
	
exists后面(相关子查询)
	表子查询

按结果集的行列数不同:
标量子查询(结果集只有一行一列)
列子查询(结果集只有一列多行)
行子查询(结果集有一行多列)
表子查询(结果集一般为多行多列)

*/

一、where或having后面

1、标量子查询(单行子查询)
2、列子查询(多行子查询)
3、行子查询(多列多行)
特点:
①子查询放在小括号内
②子查询一般放在条件的右侧
③标量子查询,一般搭配着单行操作符使用
> < >= <= = <>
列子查询,一般搭配着多行操作符使用
in、any/some、all
④子查询的执行优先于主查询执行,主查询的条件用到了子查询的结果

1.标量子查询★

#案例1:谁的工资比 Abel 高?
	#①查询Abel的工资
		SELECT salary
		FROM employees
		WHERE last_name = 'Abel'
	#②查询员工的信息,满足 salary>①结果
		SELECT *
		FROM employees
		WHERE salary>(
			SELECT salary
			FROM employees
			WHERE last_name = 'Abel'
		);

#案例2:返回job_id与141号员工相同,salary比143号员工多的员工 姓名,job_id 和工资
	#①查询141号员工的job_id
		SELECT job_id
		FROM employees
		WHERE employee_id = 141
	#②查询143号员工的salary
		SELECT salary
		FROM employees
		WHERE employee_id = 143
	#③查询员工的姓名,job_id 和工资,要求job_id=①并且salary>②
		SELECT last_name,job_id,salary
		FROM employees
		WHERE job_id = (
			SELECT job_id
			FROM employees
			WHERE employee_id = 141
		) AND salary>(
			SELECT salary
			FROM employees
			WHERE employee_id = 143
		);

#案例3:返回公司工资最少的员工的last_name,job_id和salary
	#①查询公司的 最低工资
		SELECT MIN(salary)
		FROM employees
	#②查询last_name,job_id和salary,要求salary=①
		SELECT last_name,job_id,salary
		FROM employees
		WHERE salary=(
			SELECT MIN(salary)
			FROM employees
		);

#案例4:查询最低工资大于50号部门最低工资的部门id和其最低工资
	#①查询50号部门的最低工资
		SELECT  MIN(salary)
		FROM employees
		WHERE department_id = 50
	#②查询每个部门的最低工资
		SELECT MIN(salary),department_id
		FROM employees
		GROUP BY department_id
	#③ 在②基础上筛选,满足min(salary)>①
		SELECT MIN(salary),department_id
		FROM employees
		GROUP BY department_id
		HAVING MIN(salary)>(
			SELECT  MIN(salary)
			FROM employees
			WHERE department_id = 50
		);

#非法使用标量子查询
	SELECT MIN(salary),department_id
	FROM employees
	GROUP BY department_id
	HAVING MIN(salary)>(
		SELECT  salary
		FROM employees
		WHERE department_id = 250
	);

2.列子查询(多行子查询)★

#案例1:返回location_id是1400或1700的部门中的所有员工姓名
	#①查询location_id是1400或1700的部门编号
		SELECT DISTINCT department_id
		FROM departments
		WHERE location_id IN(1400,1700)
	#②查询员工姓名,要求部门号是①列表中的某一个
		SELECT last_name
		FROM employees
		WHERE department_id  <>ALL(
			SELECT DISTINCT department_id
			FROM departments
			WHERE location_id IN(1400,1700)
		);

#案例2:返回其它工种中比job_id为‘IT_PROG’工种任一工资低的员工的员工号、姓名、job_id 以及salary
	#①查询job_id为‘IT_PROG’部门任一工资
		SELECT DISTINCT salary
		FROM employees
		WHERE job_id = 'IT_PROG'
	#②查询员工号、姓名、job_id 以及salary,salary<(①)的任意一个
		SELECT last_name,employee_id,job_id,salary
		FROM employees
		WHERE salary<ANY(
			SELECT DISTINCT salary
			FROM employees
			WHERE job_id = 'IT_PROG'
		) AND job_id<>'IT_PROG';
	#或
		SELECT last_name,employee_id,job_id,salary
		FROM employees
		WHERE salary<(
			SELECT MAX(salary)
			FROM employees
			WHERE job_id = 'IT_PROG'
		) AND job_id<>'IT_PROG';

#案例3:返回其它部门中比job_id为‘IT_PROG’部门所有工资都低的员工   的员工号、姓名、job_id 以及salary
		SELECT last_name,employee_id,job_id,salary
		FROM employees
		WHERE salary<ALL(
			SELECT DISTINCT salary
			FROM employees
			WHERE job_id = 'IT_PROG'
		) AND job_id<>'IT_PROG';
	#或
		SELECT last_name,employee_id,job_id,salary
		FROM employees
		WHERE salary<(
			SELECT MIN( salary)
			FROM employees
			WHERE job_id = 'IT_PROG'
		) AND job_id<>'IT_PROG';

3.行子查询(结果集一行多列或多行多列)

#案例:查询员工编号最小并且工资最高的员工信息
	SELECT * 
	FROM employees
	WHERE (employee_id,salary)=(
		SELECT MIN(employee_id),MAX(salary)
		FROM employees
	);
	#①查询最小的员工编号
		SELECT MIN(employee_id)
		FROM employees
	#②查询最高工资
		SELECT MAX(salary)
		FROM employees
	#③查询员工信息
		SELECT *
		FROM employees
		WHERE employee_id=(
			SELECT MIN(employee_id)
			FROM employees
		)AND salary=(
			SELECT MAX(salary)
			FROM employees
		);

二、select后面

仅仅支持标量子查询

#案例1:查询每个部门的员工个数
	SELECT d.*,(
		SELECT COUNT(*)
		FROM employees e
		WHERE e.department_id = d.`department_id`
	 ) 个数
	 FROM departments d;

 #案例2:查询员工号=102的部门名
	SELECT (
		SELECT department_name,e.department_id
		FROM departments d
		INNER JOIN employees e
		ON d.department_id=e.department_id
		WHERE e.employee_id=102
	) 部门名;

三、from后面

将子查询结果充当一张表,要求必须起别名

#案例:查询每个部门的平均工资的工资等级
	#①查询每个部门的平均工资
		SELECT AVG(salary),department_id
		FROM employees
		GROUP BY department_id
	
		SELECT * FROM job_grades;
		
	#②连接①的结果集和job_grades表,筛选条件平均工资 between lowest_sal and highest_sal
		SELECT  ag_dep.*,g.`grade_level`
		FROM (
			SELECT AVG(salary) ag,department_id
			FROM employees
			GROUP BY department_id
		) ag_dep
		INNER JOIN job_grades g
		ON ag_dep.ag BETWEEN lowest_sal AND highest_sal;

四、exists后面(相关子查询)

语法:exists(完整的查询语句)
结果:1或0

SELECT EXISTS(SELECT employee_id FROM employees WHERE salary=300000);

#案例1:查询有员工的部门名
	#in
		SELECT department_name
		FROM departments d
		WHERE d.`department_id` IN(
			SELECT department_id
			FROM employees
		)
	#exists
		SELECT department_name
		FROM departments d
		WHERE EXISTS(
			SELECT *
			FROM employees e
			WHERE d.`department_id`=e.`department_id`
		);

#案例2:查询没有女朋友的男神信息
	#in
		SELECT bo.*
		FROM boys bo
		WHERE bo.id NOT IN(
			SELECT boyfriend_id
			FROM beauty
		)
	#exists
		SELECT bo.*
		FROM boys bo
		WHERE NOT EXISTS(
			SELECT boyfriend_id
			FROM beauty b
			WHERE bo.`id`=b.`boyfriend_id`
		);

进阶8:分页查询

应用场景:当要显示的数据,一页显示不全,需要分页提交sql请求
语法:
	select 查询列表
	from 表
	【join type join 表2
	on 连接条件
	where 筛选条件
	group by 分组字段
	having 分组后的筛选
	order by 排序的字段】
	limit 【offset,】size;
offset要显示条目的起始索引(起始索引从0开始)
size 要显示的条目个数

特点:
	①limit语句放在查询语句的最后
	②公式
	要显示的页数 page,每页的条目数size
	select 查询列表
	from 表
	limit (page-1)*size,size;
#案例1:查询前五条员工信息
	SELECT * FROM  employees LIMIT 0,5;
	SELECT * FROM  employees LIMIT 5;

#案例2:查询第11条——第25条
	SELECT * FROM  employees LIMIT 10,15;

#案例3:有奖金的员工信息,并且工资较高的前10名显示出来
	SELECT 
	    * 
	FROM
	    employees 
	WHERE commission_pct IS NOT NULL 
	ORDER BY salary DESC 
	LIMIT 10 ;

进阶9:联合查询

引入:
union 联合、合并

语法:

select 字段|常量|表达式|函数 【from 表】 【where 条件】 union 【all】
select 字段|常量|表达式|函数 【from 表】 【where 条件】 union 【all】
select 字段|常量|表达式|函数 【from 表】 【where 条件】 union  【all】
.....
select 字段|常量|表达式|函数 【from 表】 【where 条件】

特点:

1、多条查询语句的查询的列数必须是一致的
2、多条查询语句的查询的列的类型几乎相同
3、union代表去重,union all代表不去重

/*
union 联合 合并:将多条查询语句的结果合并成一个结果

语法:
查询语句1
union
查询语句2
union

应用场景:
要查询的结果来自于多个表,且多个表没有直接的连接关系,但查询的信息一致时

特点:★
1、要求多条查询语句的查询列数是一致的!
2、要求多条查询语句的查询的每一列的类型和顺序最好一致
3、union关键字默认去重,如果使用union all 可以包含重复项

*/

#引入的案例:查询部门编号>90或邮箱包含a的员工信息

SELECT * FROM employees WHERE email LIKE ‘%a%’ OR department_id>90;;

SELECT * FROM employees WHERE email LIKE ‘%a%’
UNION
SELECT * FROM employees WHERE department_id>90;

#案例:查询中国用户中男性的信息以及外国用户中年男性的用户信息

SELECT id,cname FROM t_ca WHERE csex=‘男’
UNION ALL
SELECT t_id,tname FROM t_ua WHERE tGender=‘male’;



变量

一、系统变量

/*
说明:变量由系统定义,不是用户定义,属于服务器层面
注意:全局变量需要添加global关键字,会话变量需要添加session关键字,如果不写,默认会话级别
*/
使用步骤:
#1、查看所有系统变量
show global|session】variables;
#2、查看满足条件的部分系统变量
show global|session】 variables like '%char%';
#3、查看指定的系统变量的值
select @@global|session】系统变量名;
#4、为某个系统变量赋值
方式一:
set global|session】系统变量名=;
方式二:
set @@global|session】系统变量名=;

1、全局变量

/*
作用域:针对于所有会话(连接)有效,但不能跨重启
*/
#①查看所有全局变量
SHOW GLOBAL VARIABLES;
#②查看满足条件的部分系统变量
SHOW GLOBAL VARIABLES LIKE '%char%';
#③查看指定的系统变量的值
SELECT @@global.autocommit;
#④为某个系统变量赋值
SET @@global.autocommit=0;
SET GLOBAL autocommit=0;

2、会话变量

/*
作用域:针对于当前会话(连接)有效
*/
#①查看所有会话变量
SHOW SESSION VARIABLES;
#②查看满足条件的部分会话变量
SHOW SESSION VARIABLES LIKE '%char%';
#③查看指定的会话变量的值
SELECT @@autocommit;
SELECT @@session.tx_isolation;
#④为某个会话变量赋值
SET @@session.tx_isolation='read-uncommitted';
SET SESSION tx_isolation='read-committed';

二、自定义变量

/*
说明:变量由用户自定义,而不是系统提供的
使用步骤:
1、声明
2、赋值
3、使用(查看、比较、运算等)
*/

1、用户变量

/*
作用域:针对于当前会话(连接)有效,作用域同于会话变量
*/

#赋值操作符:=或:=
#①声明并初始化
SET @变量名=;
SET @变量名:=;
SELECT @变量名:=;

#②赋值(更新变量的值)
#方式一:
	SET @变量名=;
	SET @变量名:=;
	SELECT @变量名:=;
#方式二:
	SELECT 字段 INTO @变量名
	FROM;
#③使用(查看变量的值)
SELECT @变量名;

2、局部变量

/*
作用域:仅仅在定义它的begin end块中有效
应用在 begin end中的第一句话
*/

#①声明
DECLARE 变量名 类型;
DECLARE 变量名 类型 【DEFAULT 值】;


#②赋值(更新变量的值)

#方式一:
	SET 局部变量名=;
	SET 局部变量名:=;
	SELECT 局部变量名:=;
#方式二:
	SELECT 字段 INTO 具备变量名
	FROM;
#③使用(查看变量的值)
SELECT 局部变量名;


#案例:声明两个变量,求和并打印

#用户变量
SET @m=1;
SET @n=1;
SET @sum=@m+@n;
SELECT @sum;

#局部变量
DECLARE m INT DEFAULT 1;
DECLARE n INT DEFAULT 1;
DECLARE SUM INT;
SET SUM=m+n;
SELECT SUM;


#用户变量和局部变量的对比

		作用域			定义位置		语法
用户变量	当前会话		会话的任何地方		加@符号,不用指定类型
局部变量	定义它的BEGIN ENDBEGIN END的第一句话	一般不用加@,需要指定类型

存储过程与函数

存储过程和函数:类似于java中的方法
好处:
1、提高代码的重用性
2、简化操作

存储过程

含义:一组预先编译好的SQL语句的集合,理解成批处理语句
1、提高代码的重用性
2、简化操作
3、减少了编译次数并且减少了和数据库服务器的连接次数,提高了效率

一、创建语法

CREATE PROCEDURE 存储过程名(参数列表)
BEGIN
	存储过程体(一组合法的SQL语句)
END

#注意:
/*
1、参数列表包含三部分
参数模式  参数名  参数类型
举例:
in stuname varchar(20)

参数模式:
in:该参数可以作为输入,也就是该参数需要调用方传入值
out:该参数可以作为输出,也就是该参数可以作为返回值
inout:该参数既可以作为输入又可以作为输出,也就是该参数既需要传入值,又可以返回值

2、如果存储过程体仅仅只有一句话,begin end可以省略
存储过程体中的每条sql语句的结尾要求必须加分号。
存储过程的结尾可以使用 delimiter 重新设置
语法:
delimiter 结束标记
案例:
delimiter $
*/

二、调用语法

CALL 存储过程名(实参列表);

#--------------------------------案例演示-----------------------------------
#1.空参列表
#案例:插入到admin表中五条记录

SELECT * FROM admin;

DELIMITER $
CREATE PROCEDURE myp1()
BEGIN
	INSERT INTO admin(username,`password`) 
	VALUES('john1','0000'),('lily','0000'),('rose','0000'),('jack','0000'),('tom','0000');
END $


#调用
CALL myp1()$

#2.创建带in模式参数的存储过程

#案例1:创建存储过程实现 根据女神名,查询对应的男神信息

CREATE PROCEDURE myp2(IN beautyName VARCHAR(20))
BEGIN
	SELECT bo.*
	FROM boys bo
	RIGHT JOIN beauty b ON bo.id = b.boyfriend_id
	WHERE b.name=beautyName;
	

END $

#调用
CALL myp2('柳岩')$

#案例2 :创建存储过程实现,用户是否登录成功

CREATE PROCEDURE myp4(IN username VARCHAR(20),IN PASSWORD VARCHAR(20))
BEGIN
	DECLARE result INT DEFAULT 0;#声明并初始化
	
	SELECT COUNT(*) INTO result#赋值
	FROM admin
	WHERE admin.username = username
	AND admin.password = PASSWORD;
	
	SELECT IF(result>0,'成功','失败');#使用
END $

#调用
CALL myp3('张飞','8888')$


#3.创建out 模式参数的存储过程
#案例1:根据输入的女神名,返回对应的男神名

CREATE PROCEDURE myp6(IN beautyName VARCHAR(20),OUT boyName VARCHAR(20))
BEGIN
	SELECT bo.boyname INTO boyname
	FROM boys bo
	RIGHT JOIN
	beauty b ON b.boyfriend_id = bo.id
	WHERE b.name=beautyName ;
	
END $


#案例2:根据输入的女神名,返回对应的男神名和魅力值

CREATE PROCEDURE myp7(IN beautyName VARCHAR(20),OUT boyName VARCHAR(20),OUT usercp INT) 
BEGIN
	SELECT boys.boyname ,boys.usercp INTO boyname,usercp
	FROM boys 
	RIGHT JOIN
	beauty b ON b.boyfriend_id = boys.id
	WHERE b.name=beautyName ;
	
END $


#调用
CALL myp7('小昭',@name,@cp)$
SELECT @name,@cp$



#4.创建带inout模式参数的存储过程
#案例1:传入a和b两个值,最终a和b都翻倍并返回

CREATE PROCEDURE myp8(INOUT a INT ,INOUT b INT)
BEGIN
	SET a=a*2;
	SET b=b*2;
END $

#调用
SET @m=10$
SET @n=20$
CALL myp8(@m,@n)$
SELECT @m,@n$

三、删除存储过程

#语法:drop procedure 存储过程名
DROP PROCEDURE p1;
DROP PROCEDURE p2,p3;

四、查看存储过程的信息

DESC myp2;×
SHOW CREATE PROCEDURE  myp2;

函数

含义:一组预先编译好的SQL语句的集合,理解成批处理语句
1、提高代码的重用性
2、简化操作
3、减少了编译次数并且减少了和数据库服务器的连接次数,提高了效率

区别:
存储过程:可以有0个返回,也可以有多个返回,适合做批量插入、批量更新
函数:有且仅有1 个返回,适合做处理数据后返回一个结果

一、创建语法

CREATE FUNCTION 函数名(参数列表) RETURNS 返回类型
BEGIN
	函数体
END
/*
注意:
1.参数列表 包含两部分:
参数名 参数类型

2.函数体:肯定会有return语句,如果没有会报错
如果return语句没有放在函数体的最后也不报错,但不建议

return 值;
3.函数体中仅有一句话,则可以省略begin end
4.使用 delimiter语句设置结束标记

*/

二、调用语法

SELECT 函数名(参数列表)


#------------------------------案例演示----------------------------
#1.无参有返回
#案例:返回公司的员工个数
CREATE FUNCTION myf1() RETURNS INT
BEGIN

	DECLARE c INT DEFAULT 0;#定义局部变量
	SELECT COUNT(*) INTO c#赋值
	FROM employees;
	RETURN c;
	
END $

SELECT myf1()$


#2.有参有返回
#案例1:根据员工名,返回它的工资

CREATE FUNCTION myf2(empName VARCHAR(20)) RETURNS DOUBLE
BEGIN
	SET @sal=0;#定义用户变量 
	SELECT salary INTO @sal   #赋值
	FROM employees
	WHERE last_name = empName;
	
	RETURN @sal;
END $

SELECT myf2('k_ing') $

#案例2:根据部门名,返回该部门的平均工资

CREATE FUNCTION myf3(deptName VARCHAR(20)) RETURNS DOUBLE
BEGIN
	DECLARE sal DOUBLE ;
	SELECT AVG(salary) INTO sal
	FROM employees e
	JOIN departments d ON e.department_id = d.department_id
	WHERE d.department_name=deptName;
	RETURN sal;
END $

SELECT myf3('IT')$

三、查看函数

SHOW CREATE FUNCTION myf3;

四、删除函数

DROP FUNCTION myf3;

#案例
#一、创建函数,实现传入两个float,返回二者之和

CREATE FUNCTION test_fun1(num1 FLOAT,num2 FLOAT) RETURNS FLOAT
BEGIN
	DECLARE SUM FLOAT DEFAULT 0;
	SET SUM=num1+num2;
	RETURN SUM;
END $

SELECT test_fun1(1,2)$

流程控制

顺序、分支、循环

一、分支结构

1.if函数

/*
语法:if(条件,值1,值2)
功能:实现双分支
应用在begin end中或外面
*/

2.case结构

/*
语法:
情况1:类似于switch
case 变量或表达式
when 值1 then 语句1;
when 值2 then 语句2;
...
else 语句n;
end 

情况2:
case 
when 条件1 then 语句1;
when 条件2 then 语句2;
...
else 语句n;
end 

应用在begin end 中或外面
*/

3.if结构

/*
语法:
if 条件1 then 语句1;
elseif 条件2 then 语句2;
....
else 语句n;
end if;
功能:类似于多重if

只能应用在begin end 中
*/

#案例1:创建函数,实现传入成绩,如果成绩>90,返回A,如果成绩>80,返回B,如果成绩>60,返回C,否则返回D
CREATE FUNCTION test_if(score FLOAT) RETURNS CHAR
BEGIN
	DECLARE ch CHAR DEFAULT 'A';
	IF score>90 THEN SET ch='A';
	ELSEIF score>80 THEN SET ch='B';
	ELSEIF score>60 THEN SET ch='C';
	ELSE SET ch='D';
	END IF;
	RETURN ch;
END $

SELECT test_if(87)$

#案例2:创建存储过程,如果工资<2000,则删除,如果5000>工资>2000,则涨工资1000,否则涨工资500
CREATE PROCEDURE test_if_pro(IN sal DOUBLE)
BEGIN
	IF sal<2000 THEN DELETE FROM employees WHERE employees.salary=sal;
	ELSEIF sal>=2000 AND sal<5000 THEN UPDATE employees SET salary=salary+1000 WHERE employees.`salary`=sal;
	ELSE UPDATE employees SET salary=salary+500 WHERE employees.`salary`=sal;
	END IF;
END $

CALL test_if_pro(2100)$

#案例1:创建函数,实现传入成绩,如果成绩>90,返回A,如果成绩>80,返回B,如果成绩>60,返回C,否则返回D
CREATE FUNCTION test_case(score FLOAT) RETURNS CHAR
BEGIN 
	DECLARE ch CHAR DEFAULT 'A';
	
	CASE 
	WHEN score>90 THEN SET ch='A';
	WHEN score>80 THEN SET ch='B';
	WHEN score>60 THEN SET ch='C';
	ELSE SET ch='D';
	END CASE;
	
	RETURN ch;
END $

SELECT test_case(56)$

二、循环结构

/*
分类:
while、loop、repeat

循环控制:
iterate类似于 continue,继续,结束本次循环,继续下一次
leave 类似于  break,跳出,结束当前所在的循环
*/

1.while

/*
语法:
【标签:】while 循环条件 do
	循环体;
end while【 标签】;

联想:
while(循环条件){
	循环体;
}
*/

2.loop

/*
语法:
【标签:】loop
	循环体;
end loop 【标签】;
可以用来模拟简单的死循环
*/

3.repeat

/*
语法:
【标签:】repeat
	循环体;
until 结束循环的条件
end repeat 【标签】;
*/
1.没有添加循环控制语句
#案例:批量插入,根据次数插入到admin表中多条记录
DROP PROCEDURE pro_while1$
CREATE PROCEDURE pro_while1(IN insertCount INT)
BEGIN
	DECLARE i INT DEFAULT 1;
	WHILE i<=insertCount DO
		INSERT INTO admin(username,`password`) VALUES(CONCAT('Rose',i),'666');
		SET i=i+1;
	END WHILE;
END $

CALL pro_while1(100)$
/*
int i=1;
while(i<=insertcount){
	//插入
	i++;
}
*/
2.添加leave语句
#案例:批量插入,根据次数插入到admin表中多条记录,如果次数>20则停止
TRUNCATE TABLE admin$
DROP PROCEDURE test_while1$
CREATE PROCEDURE test_while1(IN insertCount INT)
BEGIN
	DECLARE i INT DEFAULT 1;
	a:WHILE i<=insertCount DO
		INSERT INTO admin(username,`password`) VALUES(CONCAT('xiaohua',i),'0000');
		IF i>=20 THEN LEAVE a;
		END IF;
		SET i=i+1;
	END WHILE a;
END $

CALL test_while1(100)$
3.添加iterate语句
#案例:批量插入,根据次数插入到admin表中多条记录,只插入偶数次
TRUNCATE TABLE admin$
DROP PROCEDURE test_while1$
CREATE PROCEDURE test_while1(IN insertCount INT)
BEGIN
	DECLARE i INT DEFAULT 0;
	a:WHILE i<=insertCount DO
		SET i=i+1;
		IF MOD(i,2)!=0 THEN ITERATE a;
		END IF;
		
		INSERT INTO admin(username,`password`) VALUES(CONCAT('xiaohua',i),'0000');
		
	END WHILE a;
END $

CALL test_while1(100)$
/*
int i=0;
while(i<=insertCount){
	i++;
	if(i%2==0){
		continue;
	}
	插入
}
*/

索引的创建与设计

索引分类

功能逻辑上说,索引主要有 4 种,分别是普通索引、唯一索引、主键索引、全文索引。
按照物理实现方式 ,索引可以分为 2 种:聚簇索引和非聚簇索引。
按照作用字段个数=进行划分,分成单列索引和联合索引。

  • 普通索引
    在创建普通索引时,不附加任何限制条件,只是用于提高查询效率。这类索引可以创建在任何数据类型中,其值是否唯一和非空,要由字段本身的完整性约束条件决定。建立索引以后,可以通过索引进行查询。例如,在表student的字段name 上建立一个普通索引,查询记录时就可以根据该索引进行查询。

  • 唯一性索引
    使用UNIQUE参数可以设置索引为唯一性索引,在创建唯一性索引时,限制该索引的值必须是唯一的,但允许有空值。在一张数据表里可以有多个唯一索引。例如,在表student的字段email中创建唯一性索引,那么字段email的值就必须是唯一的。通过唯一性索引可以更快速地确定某条记录。

  • 主键索引
    主键索引就是一种特殊的唯一性索引,在唯一索引的基础上增加了不为空的约束,也就是NOTNULL+UNIQUE,一张表里最多只有一个主键索引。这是由主键索引的物理实现方式决定的,因为数据存储在文件中只能按照一种顺序进行存储。

  • 单列索引
    在表中的单个字段上创建索引。单列索引只根据该字段进行索引。单列索引可以是普通索引,也可以是唯一性索引,还可以是全文索引。只要保证该索引只对应一个字段即可。一个表可以有多个单列索引。

  • 多列(组合、联合)索引
    多列索引是在表的多个字段组合上创建一个索引。该索引指向创建时对应的多个字段,可以通过这几个字段进行查询,但是只有查询条件中使用了这些字段中的第一个字段时才会被使用。例如,在表中的字段id、name和gender上建立一个多列索引idx_id_name_gender,只有在查询条件中使用了字段id时该索引才会被使用。使用组合索引时遵循最左前缀集合。

  • 全文索引
    全文索引(也称全文检索)是目前搜索引擎使用的一种关键技术。它能够利用【分词技术】等多种算法智能分析出文本文字中关键词的频率和重要性,然后按照一定的算法规则智能地筛选出我们想要的搜索结果。全文索引非常适合大型数据集,对于小的数据集,它的用处比较小。

  • 空间索引
    使用参数SPATIAL可以设置索引为空间索引。空间索引只能建立在空间数据类型上,这样可以提高系统获取空间数据的效率。MySQL中的空间数据类型包括GEOMNETRY、POINT、LINESTRING和POLYGN等。目前只有MylSAM存储引擎支持空间检索,而且索引的字段不能为空值。对于初学者来说,这类索引很少会用到。

  • 不同的存储引擎支持的索引类型

InnoDB :支持 B-tree、Full-text 等索引,不支持 Hash索引;
MyISAM : 支持 B-tree、Full-text 等索引,不支持 Hash 索引;
Memory :支持 B-tree、Hash 等索引,不支持 Full-text 索引;
NDB :支持 Hash 索引,不支持 B-tree、Full-text 等索引;
Archive :不支持 B-tree、Hash、Full-text 等索引;

创建索引

MySQL支持多种方法在单个或多个列上创建索引:在创建表的定义语句CREATE TABLE中指定索引列,使用ALTER TABLE语句在存在的表上创建索引,或者使用CREATE INDEX语句在已存在的表上添加索引。

创建表的时候创建索引

使用CREATE TABLE创建表时,除了可以定义列的数据类型外,还可以定义主键约束、外键约束或者唯一性约束而不论创建哪种约束,在定义约束的同时相当于在指定列上创建了一个索引。

在声明有主键约束、唯一性约束、外键约束的字段上,会自动的添加相关的索引

CREATE TABLE dept( 
	dept_id INT PRIMARY KEY AUTO_INCREMENT, 
	dept_name VARCHAR(20) 
);

CREATE TABLE emp( 
	emp_id INT PRIMARY KEY AUTO_INCREMENT, 
	emp_name VARCHAR(20) UNIQUE, 
	dept_id INT, 
	CONSTRAINT emp_dept_id_fk FOREIGN KEY(dept_id) REFERENCES dept(dept_id) 
);

但是,如果显式创建表时创建索引的话,基本语法格式如下:

CREATE TABLE table_name [col_name data_type] 
[UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name [length]) [ASC | DESC]

UNIQUEFULLTEXTSPATIAL 为可选参数,分别表示唯一索引、全文索引和空间索引;
INDEXKEY 为同义词,两者的作用相同,用来指定创建索引;
index_name 指定索引的名称,为可选参数,如果不指定,那么MySQL默认col_name为索引名;
col_name 为需要创建索引的字段列,该列必须从数据表中定义的多个列中选择;
length 为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度;
ASCDESC 指定升序或者降序的索引值存储。

  • 创建普通索引
    在book表中的year_publication字段上建立普通索引,SQL语句如下:
CREATE TABLE book( 
	book_id INT , 
	book_name VARCHAR(100), 
	authors VARCHAR(100), 
	info VARCHAR(100) , 
	comment VARCHAR(100), 
	year_publication YEAR, 
	INDEX(year_publication) 
);

使用EXPLAIN语句查看索引是否正在使用:
EXPLAIN SELECT * FROM book WHERE year_publication = '1990';

(1) possible_keys行给出了MySQL在搜索数据记录时可选用的各个索引。
(2) key行是MySQL实际选用的索引。
可以看到,possible_keys和key的值都为year_publication,查询时使用了索引。

  • 创建唯一索引
    创建唯一索引的目的也是减少查询索引列操作的执行时间,尤其是对比较庞大的数据表。它与前面的普通索引类似,不同的是 :索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
CREATE TABLE test1( 
	id INT NOT NULL, 
	name varchar(30) NOT NULL, 
	UNIQUE INDEX uk_idx_id(id) 
);

该语句执行完毕之后,使用SHOW CREATE TABLE查看表结构:
SHOW INDEX FROM test1 \G
其中各个主要参数的含义为:
(1) Table表示创建索引的表。
(2) Non_unique表示索引非唯一,1代表非唯一索引,o代表唯一索引。
(3) Key_name表示索引的名称。
(4) Seq_in_index表示该字段在索引中的位置,单列索引该值为1,组合索引为每个字段在索引定义中的顺序.
(5) Column_name表示定义索引的列字段。
(6) Sub_part表示索引的长度。
(7)Null表示该字段是否能为空值。
(8) lndex_type表示索引类型。

由结果可以看到,id字段上已经成功建立了一个名为uk_idx_id的唯一索引。

  • 主键索引
    设定为主键后数据库会自动建立索引,innodb为聚簇索引
CREATE TABLE student ( 
	id INT(10) UNSIGNED AUTO_INCREMENT , 
	student_no VARCHAR(200), 
	student_name VARCHAR(200), 
	PRIMARY KEY(id) 
);

删除主键索引:
ALTER TABLE student drop PRIMARY KEY ;
修改主键索引:必须先删除掉(drop)原索引,再新建(add)索引

  • 创建单列索引
    单列索引是在数据表中的某一个字段上创建的索引,一个表中可以创建多个单列索引。前面例子中创建的索引都为单列索引。
CREATE TABLE test2( 
	id INT NOT NULL, 
	name CHAR(50) NULL, 
	INDEX single_idx_name(name(20)) 
);

该语句执行完毕之后,使用SHOW CREATE TABLE查看表结构:
SHOW INDEX FROM test2 \G
由结果可以看到,id字段上已经成功建立了一个名为single_idx_name的单列索引,索引长度为20。

  • 创建组合索引
    组合索引是在多个字段上创建一个索引。
# 创建表test3,在表中的id、name和age字段上建立组合索引
CREATE TABLE test3( 
	id INT(11) NOT NULL, 
	name CHAR(30) NOT NULL, 
	age INT(11) NOT NULL, 
	info VARCHAR(255), 
	INDEX multi_idx(id,name,age) 
);

该语句执行完毕之后,使用SHOW INDEX 查看:
SHOW INDEX FROM test3 \G
由结果可以看到,id、name和age字段上已经成功建立了一个名为multi_idx的组合索引。

组合索引可起几个索引的作用,但是使用时并不是随便查询哪个字段都可以使用索引,而是遵从“最左前缀”。例如,索引可以搜索的字段组合为: (id, name, age) . (id, name)或者id。而(age)或者(name,age)组合不能使用索引查询。

在test3表中,查询id和name字段,使用EXPLAIN语句查看索引的使用情况:

EXPLAIN SELECT * FROM test3 WHERE id =1 And name ='songkongkang'\G;

可以看到,查询id和name字段时,使用了名称为Multildx的索引,如果查询(name,age)组合或者单独查询name和age字段,会发现结果中possible_keys和key值为NULL,并没有使用在t3表中创建的索引进行查询。

  • 创建全文索引
    FULLTEXT全文索引可以用于全文搜索,并且只为CHAR、VARCHAR和TEXT列创建索引。索引总是对整个列进行,不支持局部(前缀)索引。
# 创建表test4,在表中的info字段上建立全文索引
CREATE TABLE test4( 
	id INT NOT NULL, 
	name CHAR(30) NOT NULL, 
	age INT NOT NULL, 
	info VARCHAR(255), 
	FULLTEXT INDEX futxt_idx_info(info) 
) ENGINE=MyISAM;

在MySQL5.7及之后版本中可以不指定最后的ENGINE了,因为在此版本中InnoDB支持全文索引。
语句执行完毕之后,使用SHOW CREATE TABLE查看表结构:
SHOW INDEX FROM test4 \G
由结果可以看到,info字段上已经成功建立了一个名为futxt_idx_info的FULLTEXT索引。

# 创建了一个给title和body字段添加全文索引的表
CREATE TABLE articles ( 
	id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 
	title VARCHAR (200), 
	body TEXT, 
	FULLTEXT index (title, body) 
) ENGINE = INNODB ;
CREATE TABLE `papers` ( 
	`id` int(10) unsigned NOT NULL AUTO_INCREMENT, 
	`title` varchar(200) DEFAULT NULL, 
	`content` text, 
	PRIMARY KEY (`id`), 
	FULLTEXT KEY `title` (`title`,`content`) 
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

不同于like方式的的查询:
SELECT * FROM papers WHERE content LIKE ‘%查询字符串%’;
全文索引用match+against方式查询:
SELECT * FROM papers WHERE MATCH(title,content) AGAINST (‘查询字符串’);

  • 注意点

使用全文索引前,搞清楚版本支持情况;
全文索引比 like + % 快 N 倍,但是可能存在精度问题;
如果需要全文索引的是大量数据,建议先添加数据,再创建索引。

  • 创建空间索引
    空间索引创建中,要求空间类型的字段必须为 非空 。
#创建表test5,在空间类型为GEOMETRY的字段上创建空间索引
CREATE TABLE test5( 
	geo GEOMETRY NOT NULL, 
	SPATIAL INDEX spa_idx_geo(geo) 
) ENGINE=MyISAM;

在已经存在的表上创建索引

在已经存在的表中创建索引可以使用ALTER TABLE语句或者CREATE INDEX语句。

使用ALTER TABLE语句创建索引 ALTER TABLE语句创建索引的基本语法如下:

ALTER TABLE table_name ADD [UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name]
 (col_name[length],...) [ASC | DESC]

使用CREATE INDEX创建索引 CREATE INDEX语句可以在已经存在的表上添加索引,在MySQL中,
CREATE INDEX被映射到一个ALTER TABLE语句上,基本语法结构为:

CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name ON table_name 
(col_name[length],...) [ASC | DESC]

删除索引

MysQL中删除索引使用ALTER TABLE或者DROP INDEX语句,两者可实现相同的功能,DROP INDEX语句在内部被映射到一个ALTER TABLE语句中。

  • 使用ALTER TABLE删除索引
    ALTER TABLE删除索引的基本语法格式如下:
    ALTER TABLE table_name DROP INDEX index_name;

删除book表中名称为idx_bk_id的唯一索引。

# 查看book表中是否有名称为idx_bk_id的索引
SHOW INDEX FROM book\G
# 删除该索引
ALTER TABLE book DROP INDEX idx_bk_id ;

提示:添加AUTO_INCREMENT约束字段的唯一索引不能被删除。

  • 使用DROP INDEX语句删除索引
    DROP INDEX删除索引的基本语法格式如下:
    DROP INDEX index_name ON table_name;

提示 删除表中的列时,如果要删除的列为索引的组成部分,则该列也会从索引中删除。如果组成
索引的所有列都被删除,则整个索引将被删除。

  1. MySQL8.0索引新特性
    2.1 支持降序索引
    降序索引以降序存储键值。虽然在语法上,从MysQL 4版本开始就已经支持降序索引的语法了,但实际上该DESC定义是被忽略的,直到MysQL8.x版本才开始真正支持降序索引(仅限于InnoDB存储引擎)。

MysQL在8.o版本之前创建的仍然是升序索引,使用时进行反向扫描,这大大降低了数据库的效率。在某些场景下,降序索引意义重大。例如,如果一个查询,需要对多个列进行排序,且顺序要求不一致,那么使用降序索引将会避免数据库使用额外的文件排序操作,从而提高性能。

举例:分别在MySQL 5.7版本和MySQL 8.0版本中创建数据表ts1,结果如下:

CREATE TABLE ts1(a int,b int,index idx_a_b(a,b desc));
sql
CREATE TABLE ts1(a int,b int,index idx_a_b(a,b desc));

在MySQL 5.7版本中查看数据表ts1的结构,结果如下:

图片.png

从结果可以看出,索引仍然是默认的升序。

在MySQL 8.0版本中查看数据表ts1的结构,结果如下:

图片.png

从结果可以看出,索引已经是降序了。下面继续测试降序索引在执行计划中的表现。

分别在MySQL 5.7版本和MySQL 8.0版本的数据表ts1中插入800条随机数据,执行语句如下:

DELIMITER //
CREATE PROCEDURE ts_insert()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i < 800
DO
insert into ts1 select rand()*80000,rand()*80000;
SET i = i + 1;
END WHILE;
commit;
END //
DELIMITER ;

#调用

CALL ts_insert();
sql
DELIMITER //
CREATE PROCEDURE ts_insert()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i < 800
DO
insert into ts1 select rand()*80000,rand()*80000;
SET i = i + 1;
END WHILE;
commit;
END //
DELIMITER ;

#调用

CALL ts_insert();

在MySQL 5.7版本中查看数据表ts1的执行计划,结果如下:

EXPLAIN SELECT * FROM ts1 ORDER BY a,b DESC LIMIT 5;
sql
EXPLAIN SELECT * FROM ts1 ORDER BY a,b DESC LIMIT 5;

从结果可以看出,执行计划中扫描数为799,而且使用了Using filesort。

提示 Using filesort是MySQL中一种速度比较慢的外部排序,能避免是最好的。多数情况下,管理员
可以通过优化索引来尽量避免出现Using filesort,从而提高数据库执行速度。

在MySQL 8.0版本中查看数据表ts1的执行计划。从结果可以看出,执行计划中扫描数为5,而且没有使用
Using filesort。

注意 降序索引只对查询中特定的排序顺序有效,如果使用不当,反而查询效率更低。例如,上述
查询排序条件改为order by a desc, b desc,MySQL 5.7的执行计划要明显好于MySQL 8.0。

将排序条件修改为order by a desc, b desc后,下面来对比不同版本中执行计划的效果。 在MySQL 5.7版本
中查看数据表ts1的执行计划,结果如下:

EXPLAIN SELECT * FROM ts1 ORDER BY a DESC,b DESC LIMIT 5;
sql
EXPLAIN SELECT * FROM ts1 ORDER BY a DESC,b DESC LIMIT 5;

在MySQL 8.0版本中查看数据表ts1的执行计划。

从结果可以看出,修改后MySQL 5.7的执行计划要明显好于MySQL 8.0。

隐藏索引

在MySQL 5.7版本及之前,只能通过显式的方式删除索引。此时,如果发现删除索引后出现错误,又只能通过显式创建索引的方式将删除的索引创建回来。如果数据表中的数据量非常大,或者数据表本身比较大,这种操作就会消耗系统过多的资源,操作成本非常高。

从MySQL 8.x开始支持 隐藏索引(invisible indexes) ,只需要将待删除的索引设置为隐藏索引,使
查询优化器不再使用这个索引(即使使用force index(强制使用索引),优化器也不会使用该索引),
确认将索引设置为隐藏索引后系统不受任何响应,就可以彻底删除索引。 这种通过先将索引设置为隐藏索引,再删除索引的方式就是软删除 。

同时,如果你想验证某个索引删除之后的查询性能影响,就可以暂时先隐藏该索引。

注意:
主键不能被设置为隐藏索引。当表中没有显式主键时,表中第一个唯一非空索引会成为隐式主键,也不能设置为隐藏索引。

索引默认是可见的,在使用CREATE TABLE,CREATE INDEX或者ALTER TABLE等语句时可以通过 VISIBLE 或者INVISIBLE关键词设置索引的可见性。

  • 创建表时直接创建
在MySQL中创建隐藏索引通过SQL语句INVISIBLE来实现
CREATE TABLE tablename( 
	propname1 type1[CONSTRAINT1], 
	propname2 type2[CONSTRAINT2], 
	……
	propnamen typen, 
	INDEX [indexname](propname1 [(length)]) INVISIBLE 
);

上述语句比普通索引多了一个关键字INVISIBLE,用来标记索引为不可见索引。

  • 在已经存在的表上创建
    CREATE INDEX indexname ON tablename(propname[(length)]) INVISIBLE;

ALTER TABLE tablename ADD INDEX indexname (propname [(length)]) INVISIBLE;

切换索引可见状态
ALTER TABLE tablename ALTER INDEX index_name INVISIBLE; #切换成隐藏索引

ALTER TABLE tablename ALTER INDEX index_name VISIBLE; #切换成非隐藏索引

如果将index_cname索引切换成可见状态,通过explain查看执行计划,发现优化器选择了index_cname索引。

注意 当索引被隐藏时,它的内容仍然是和正常索引一样实时更新的。如果一个索引需要长期被隐藏,那么可以将其删除,因为索引的存在会影响插入、更新和删除的性能。

通过设置隐藏索引的可见性可以查看索引对调优的帮助。

使隐藏索引对查询优化器可见
在MySQL 8.x版本中,为索引提供了一种新的测试方式,可以通过查询优化器的一个开关
(use_invisible_indexes)来打开某个设置,使隐藏索引对查询优化器可见。如果use_invisible_indexes
设置为off(默认),优化器会忽略隐藏索引。如果设置为on,即使隐藏索引不可见,优化器在生成执行计
划时仍会考虑使用隐藏索引。

(1)在MySQL命令行执行如下命令查看查询优化器的开关设置。

mysql> select @@optimizer_switch \G

在输出的结果信息中找到如下属性配置。
use_invisible_indexes=off

此属性配置值为off,说明隐藏索引默认对查询优化器不可见。

(2)使隐藏索引对查询优化器可见,需要在MySQL命令行执行如下命令:

mysql> set session optimizer_switch=“use_invisible_indexes=on”;

SQL语句执行成功,再次查看查询优化器的开关设置。

mysql> select @@optimizer_switch \G
*************************** 1. row ***************************
@@optimizer_switch: index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_
intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,
mrr_co st_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,
semijoin=on ,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on
,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on,use_invisible_ind
exes=on,skip_scan=on,hash_join=on 1 row in set (0.00 sec)

此时,在输出结果中可以看到如下属性配置。

use_invisible_indexes=on
use_invisible_indexes属性的值为on,说明此时隐藏索引对查询优化器可见。

(3)使用EXPLAIN查看以字段invisible_column作为查询条件时的索引使用情况。

explain select * from classes where cname = ‘高一2班’;

查询优化器会使用隐藏索引来查询数据。

(4)如果需要使隐藏索引对查询优化器不可见,则只需要执行如下命令即可。

mysql> set session optimizer_switch=“use_invisible_indexes=off”;
Query OK, 0 rows affected (0.00 sec)

再次查看查询优化器的开关设置。

mysql> select @@optimizer_switch \G

此时,use_invisible_indexes属性的值已经被设置为“off”。

索引的设计原则

为了使索引的使用效率更高,在创建索引时,必须考虑在哪些字段上创建索引和创建什么类型的索引。索引设计不合理或者缺少索引都会对数据库和应用程序的性能造成障碍。高效的索引对于获得良好的性能非常重要。设计索引时,应该考虑相应准则。

数据准备

1步:创建数据库、创建表
CREATE DATABASE atguigudb1; 

USE atguigudb1; 

#1.创建学生表和课程表 
CREATE TABLE `student_info` ( 
	`id` INT(11) NOT NULL AUTO_INCREMENT, 
	`student_id` INT NOT NULL , 
	`name` VARCHAR(20) DEFAULT NULL, 
	`course_id` INT NOT NULL , 
	`class_id` INT(11) DEFAULT NULL, 
	`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 
	PRIMARY KEY (`id`) 
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 

CREATE TABLE `course` ( 
	`id` INT(11) NOT NULL AUTO_INCREMENT, 
	`course_id` INT NOT NULL ,
	 `course_name` VARCHAR(40) DEFAULT NULL, 
	PRIMARY KEY (`id`) 
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
sql
CREATE DATABASE atguigudb1; 

USE atguigudb1; 

#1.创建学生表和课程表 
CREATE TABLE `student_info` ( 
	`id` INT(11) NOT NULL AUTO_INCREMENT, 
	`student_id` INT NOT NULL , 
	`name` VARCHAR(20) DEFAULT NULL, 
	`course_id` INT NOT NULL , 
	`class_id` INT(11) DEFAULT NULL, 
	`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 
	PRIMARY KEY (`id`) 
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 

CREATE TABLE `course` ( 
	`id` INT(11) NOT NULL AUTO_INCREMENT, 
	`course_id` INT NOT NULL ,
	 `course_name` VARCHAR(40) DEFAULT NULL, 
	PRIMARY KEY (`id`) 
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;2步:创建模拟数据必需的存储函数
#函数1:创建随机产生字符串函数 

DELIMITER // 

CREATE FUNCTION rand_string(n INT) 
	RETURNS VARCHAR(255) #该函数会返回一个字符串 
BEGIN
	DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
	 DECLARE return_str VARCHAR(255) DEFAULT '';
	DECLARE i INT DEFAULT 0; 
	WHILE i < n DO 
		SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
		 SET i = i + 1; 
	END WHILE; 
	RETURN return_str; 
END // 
DELIMITER ;
sql
#函数1:创建随机产生字符串函数 

DELIMITER // 

CREATE FUNCTION rand_string(n INT) 
	RETURNS VARCHAR(255) #该函数会返回一个字符串 
BEGIN
	DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
	 DECLARE return_str VARCHAR(255) DEFAULT '';
	DECLARE i INT DEFAULT 0; 
	WHILE i < n DO 
		SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
		 SET i = i + 1; 
	END WHILE; 
	RETURN return_str; 
END // 
DELIMITER ;

#函数2:创建随机数函数 

DELIMITER // 
CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11) 
BEGIN 
DECLARE i INT DEFAULT 0; 
SET i = FLOOR(from_num +RAND()*(to_num - from_num+1)) ; 
RETURN i; 
END // 
DELIMITER ;
sql
#函数2:创建随机数函数 

DELIMITER // 
CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11) 
BEGIN 
DECLARE i INT DEFAULT 0; 
SET i = FLOOR(from_num +RAND()*(to_num - from_num+1)) ; 
RETURN i; 
END // 
DELIMITER ;

创建函数,假如报错:

This function has none of DETERMINISTIC......
sql
This function has none of DETERMINISTIC......

由于开启过慢查询日志bin-log, 我们就必须为我们的function指定一个参数。

主从复制,主机会将写操作记录在bin-log日志中。从机读取bin-log日志,执行语句来同步数据。如果使
用函数来操作数据,会导致从机和主键操作时间不一致。所以,默认情况下,mysql不开启创建函数设
置。

查看mysql是否允许创建函数:
show variables like 'log_bin_trust_function_creators';
sql
show variables like 'log_bin_trust_function_creators';

命令开启:允许创建函数设置:
set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。
sql
set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。

mysqld重启,上述参数又会消失。永久方法:

windows下:my.ini[mysqld]加上:
log_bin_trust_function_creators=1

sql
log_bin_trust_function_creators=1


linux下:/etc/my.cnf下my.cnf[mysqld]加上:
log_bin_trust_function_creators=1
sql
log_bin_trust_function_creators=13步:创建插入模拟数据的存储过程
# 存储过程1:创建插入课程表存储过程 
DELIMITER // 
CREATE PROCEDURE insert_course( max_num INT ) 
BEGIN 
DECLARE i INT DEFAULT 0; 
SET autocommit = 0; #设置手动提交事务 
REPEAT #循环 
SET i = i + 1; #赋值 
INSERT INTO course (course_id, course_name ) VALUES (rand_num(10000,10100),rand_string(6)); 
UNTIL i = max_num
END REPEAT; 
COMMIT; #提交事务 
END // 
DELIMITER ;
sql
# 存储过程1:创建插入课程表存储过程 
DELIMITER // 
CREATE PROCEDURE insert_course( max_num INT ) 
BEGIN 
DECLARE i INT DEFAULT 0; 
SET autocommit = 0; #设置手动提交事务 
REPEAT #循环 
SET i = i + 1; #赋值 
INSERT INTO course (course_id, course_name ) VALUES (rand_num(10000,10100),rand_string(6)); 
UNTIL i = max_num
END REPEAT; 
COMMIT; #提交事务 
END // 
DELIMITER ;

# 存储过程2:创建插入学生信息表存储过程 

DELIMITER // 
CREATE PROCEDURE insert_stu( max_num INT ) 
BEGIN 
DECLARE i INT DEFAULT 0; 
SET autocommit = 0; #设置手动提交事务 
REPEAT #循环 
SET i = i + 1; #赋值 
INSERT INTO student_info (course_id, class_id ,student_id ,NAME ) VALUES
 (rand_num(10000,10100),rand_num(10000,10200),rand_num(1,200000),rand_string(6)); 
UNTIL i = max_num 
END REPEAT; 
COMMIT; #提交事务 
END // 
DELIMITER ;
sql
# 存储过程2:创建插入学生信息表存储过程 

DELIMITER // 
CREATE PROCEDURE insert_stu( max_num INT ) 
BEGIN 
DECLARE i INT DEFAULT 0; 
SET autocommit = 0; #设置手动提交事务 
REPEAT #循环 
SET i = i + 1; #赋值 
INSERT INTO student_info (course_id, class_id ,student_id ,NAME ) VALUES
 (rand_num(10000,10100),rand_num(10000,10200),rand_num(1,200000),rand_string(6)); 
UNTIL i = max_num 
END REPEAT; 
COMMIT; #提交事务 
END // 
DELIMITER ;4步:调用存储过程
CALL insert_course(100); 

CALL insert_stu(1000000);
sql
CALL insert_course(100); 

CALL insert_stu(1000000);

哪些情况适合创建索引

字段的数值有唯一性的限制

索引本身可以起到约束的作用,比如唯一索引、主键索引都是可以起到唯一性约束的,因此在我们的数据表中如果某个字段是唯一性的,就可以直接创建唯一性索引,或者主键索引。这样可以更快速地通过该索引来确定某条记录。

例如,学生表中学号是具有唯一性的字段,为该字段建立唯一性索引可以很快确定某个学生的信息,如果使用姓名的话,可能存在同名现象,从而降低查询速度。

业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。(来源:Alibaba)
说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的。

频繁作为 WHERE 查询条件的字段

某个字段在SELECT语句的 WHERE 条件中经常被使用到,那么就需要给这个字段创建索引了。尤其是在
数据量大的情况下,创建普通索引就可以大幅提升数据查询的效率。

比如student_info数据表(含100万条数据),假设我们想要查询 student_id=123110 的用户信息。

如果我们没有对student_id字段创建索引,进行如下查询:

SELECT course_id,class_id,name,create_time,student_id FROM student_info
WHERE student_id = 123110;
sql
SELECT course_id,class_id,name,create_time,student_id FROM student_info
WHERE student_id = 123110;

运行结果:

经常 GROUP BY 和 ORDER BY 的列

索引就是让数据按照某种顺序进行存储或检索,因此当我们使用 GROUP BY 对数据进行分组查询,或者
使用 ORDER BY 对数据进行排序的时候,就需要 对分组或者排序的字段进行索引 。如果待排序的列有多
个,那么可以在这些列上建立 组合索引 。

比如,按照student_id对学生选修的课程进行分组,显示不同的student_id和课程数量,显示100个即可。如果我们不对student_id创建索引,执行下面的sQL语句:

SELECT student_id,count() as num FROM student_info group by student_id limit 100;
sql
SELECT student_id,count(
) as num FROM student_info group by student_id limit 100;

运行结果(100条记录,运行时间0.56s ) :

mysql> SELECT student_id,count() as num FROM student_info group by student_id limit 100;
100 rows in set (0.56 sec)
sql
mysql> SELECT student_id,count(
) as num FROM student_info group by student_id limit 100;
100 rows in set (0.56 sec)

如果我们对student_id创建索引,再执行sQL语句。结果如下:

mysql> SELECT student_id,COUNT(*) AS num
->FROM student_info
->GROUP BY student_id LIMIT 100;
100 rows in set (0.80 sec)

sql
mysql> SELECT student_id,COUNT(*) AS num
->FROM student_info
->GROUP BY student_id LIMIT 100;
100 rows in set (0.80 sec)

运行结果(100条记录,运行时间0.00s ),效率提升很明显。而且,得到的结果中student_id字段的数值也是按照顺序展示的。

同样,如果是ORDER BY,也需要对字段创建索引。
如果同时有GROUP BY和ORDER BY的情况:比如我们按照student_id进行分组,同时按照创建时间降序的方式进行排序,这时我们就需要同时进行GROUP BY和ORDER BY,那么是不是需要单独创建student_id的索引和create_time的索引呢?

当我们对student_id和create_time分别创建索引,执行下面的sQL查询:

SELECT student_id,count(* ) as num FROM student_info group by student_id
order by create_time desc limit 100;
sql
SELECT student_id,count(* ) as num FROM student_info group by student_id
order by create_time desc limit 100;

运行结果:

mysql> SELECT student_id,COUNT(* ) AS num
->FROM student_info
->GROUP BY student_id
->ORDER BY create_time DESC
->LIMIT 100;
sql
mysql> SELECT student_id,COUNT(* ) AS num
->FROM student_info
->GROUP BY student_id
->ORDER BY create_time DESC
->LIMIT 100;

说明:多个单列索引在多条件查询时只会生效一个索引(MySQL会选择其中一个限制最严格的作为索引),所以在多条件联合查询的时候最好创键联合索引。接着,我们创建联合索引(student_id, create_time),查询时间为0.22s,效率提升了很多。

如果我们创建联合索引的顺序为(create_time, student_id)呢?运行时间为2.164s,因为在进行SELECT查询的时候,先进行GROUP BY,再对数据进行ORDER BY的操作,所以按照(student_id, create_time)这个联合索引的顺序效率是最高的。

UPDATE、DELETE 的 WHERE 条件列

当我们对某条数据进行UPDATE或者DELETE 操作的时候,是否也需要对WHERE的条件列创建索引呢?

我们先看一下对数据进行UPDATE的情况: 我们想要把 name为462eed7ac6e791292a79对应的student_id修改为10002,当我们没有对name进行索引的时候,执行SQL语句:

UPDATE student_info SET student_id = 10002
WHERE name = ‘462eed7ac6e791292a79’
sql
UPDATE student_info SET student_id = 10002
WHERE name = ‘462eed7ac6e791292a79’

运行结果为Affected rows:1,运行时间为0.578s。

你能看到效率不高,但如果我们对name字段创建了索引,然后执行类似的sQL语句:

UPDATE student_info SET student_id = 10001
WHERE name = ‘462eed7ac6e791292a79’
sql
UPDATE student_info SET student_id = 10001
WHERE name = ‘462eed7ac6e791292a79’

运行结果为Affected rows:1,运行时间仅为0.001s。效率有了大幅的提升。

如果我们对某条数据进行DELETE,效率如何呢?

比如我们想删除name为462eed7ac6e791292a79的数据。当我们没有对name字段进行索引的时候,执行sQL语句:

DELETE FROM student_info WHERE name = ‘462eed7ac6e791292a79’;
sql
DELETE FROM student_info WHERE name = ‘462eed7ac6e791292a79’;

运行结果为Affected rows: 1,运行时间为0.627s,效率不高。

如果我们对name创建了索引,再来执行这条SQL语句,运行时间为 0.03s,效率有了大幅的提升。

对数据按照某个条件进行查询后再进行 UPDATE 或 DELETE 的操作,如果对 WHERE 字段创建了索引,就
能大幅提升效率。原理是因为我们需要先根据 WHERE 条件列检索出来这条记录,然后再对它进行更新或
删除。如果进行更新的时候,更新的字段是非索引字段,提升的效率会更明显,这是因为非索引字段更 新不需要对索引进行维护。

DISTINCT 字段需要创建索引

有时候我们需要对某个字段进行去重,使用 DISTINCT,那么对这个字段创建索引,也会提升查询效率。
比如,我们想要查询课程表中不同的 student_id 都有哪些,如果我们没有对 student_id 创建索引,执行
SQL 语句:

SELECT DISTINCT(student_id) FROM student_info;
sql
SELECT DISTINCT(student_id) FROM student_info;

运行结果(600637 条记录,运行时间 0.683s ):

如果我们对 student_id 创建索引,再执行 SQL 语句:

SELECT DISTINCT(student_id) FROM student_info;
sql
SELECT DISTINCT(student_id) FROM student_info;

运行结果(600637 条记录,运行时间 0.010s ):

你能看到 SQL 查询效率有了提升,同时显示出来的 student_id 还是按照 递增的顺序 进行展示的。这是因
为索引会对数据按照某种顺序进行排序,所以在去重的时候也会快很多。

多表 JOIN 连接操作时,创建索引注意事项

首先, 连接表的数量尽量不要超过 3 张 ,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增
长会非常快,严重影响查询的效率。

其次, 对 WHERE 条件创建索引 ,因为 WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下,
没有 WHERE 条件过滤是非常可怕的。

最后, 对用于连接的字段创建索引 ,并且该字段在多张表中的 类型必须一致 。比如 course_id 在 student_info 表和 course 表中都为 int(11) 类型,而不能一个为 int 另一个为 varchar 类型。

举个例子,如果我们只对 student_id 创建索引,执行 SQL 语句:

SELECT course_id, name, student_info.student_id, course_name FROM student_info JOIN course
ON student_info.course_id = course.course_id WHERE name = ‘462eed7ac6e791292a79’;
sql
SELECT course_id, name, student_info.student_id, course_name FROM student_info JOIN course
ON student_info.course_id = course.course_id WHERE name = ‘462eed7ac6e791292a79’;

运行结果(1 条数据,运行时间 0.189s ):

这里我们对 name 创建索引,再执行上面的 SQL 语句,运行时间为 0.002s 。

使用列的类型小的创建索引

我们这里所说的类型大小指的就是该类型表示的数据范围的大小。

我们在定义表结构的时候要显式的指定列的类型,以整数类型为例,有TINYINT、MEDIUMINT、INT、BIGINT等,它们占用的存储空间依次递增,能表示的整数范围当然也是依次递增。如果我们想要对某个整数列建立索引的话,在表示的整数范围允许的情况下,尽量让索引列使用较小的类型,比如我们能使用INT就不要使用BIGINT,能使用MEDIUMINT 就不要使用INT。这是因为:

数据类型越小,在查询时进行的比较操作越快
数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘Ⅰ/O带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率。
这个建议对于表的主键来说更加适用,因为不仅是聚簇索引中会存储主键值,其他所有的二级索引的节点处都会存储一份记录的主键值,如果主键使用更小的数据类型,也就意味着节省更多的存储空间和更高效的I/O。

使用字符串前缀创建索引

假设我们的字符串很长,那存储一个字符串就需要占用很大的存储空间。在我们需要为这个字符串列建立索引时,那就意味着在对应的B+树中有这么两个问题:

B+树索引中的记录需要把该列的完整字符串存储起来,更费时。而且字符串越长,在索引中占用的存储空间越大。
如果B+树索引中索引列存储的字符串很长,那在做字符串比较时会占用更多的时间。
我们可以通过截取字段的前面一部分内容建立索引,这个就叫前缀索引。这样在查找记录时虽然不能精确的定位到记录的位置,但是能定位到相应前缀所在的位置,然后根据前缀相同的记录的主键值回表查询完整的字符串值。既节约空间,又减少了字符串的比较时间,还大体能解决排序的问题。

例如,TEXT和BLOG类型的字段,进行全文检索会很浪费时间,如果只检索字段前面的若干字符,这样可以提高检索速度。

创建一张商户表,因为地址字段比较长,在地址字段上建立前缀索引

create table shop(
address varchar(120) not null
);

alter table shop add index(address(12));
sql
create table shop(
address varchar(120) not null
);

alter table shop add index(address(12));

问题是,截取多少呢?截取得多了,达不到节省索引存储空间的目的;截取得少了,重复内容太多,字
段的散列度(选择性)会降低。怎么计算不同的长度的选择性呢?

先看一下字段在全部数据中的选择度:

select count(distinct address) / count() from shop;
sql
select count(distinct address) / count(
) from shop;

通过不同长度去计算,与全表的选择性对比:

公式:

count(distinct left(列名, 索引长度))/count()
sql
count(distinct left(列名, 索引长度))/count(
)

例如:

select count(distinct left(address,10)) / count() as sub10, – 截取前10个字符的选择度
count(distinct left(address,15)) / count(
) as sub11, – 截取前15个字符的选择度
count(distinct left(address,20)) / count() as sub12, – 截取前20个字符的选择度
count(distinct left(address,25)) / count(
) as sub13 – 截取前25个字符的选择度
from shop;
sql
select count(distinct left(address,10)) / count() as sub10, – 截取前10个字符的选择度
count(distinct left(address,15)) / count(
) as sub11, – 截取前15个字符的选择度
count(distinct left(address,20)) / count() as sub12, – 截取前20个字符的选择度
count(distinct left(address,25)) / count(
) as sub13 – 截取前25个字符的选择度
from shop;

引申另一个问题:索引列前缀对排序的影响

如果使用了索引列前缀,比方说前边只把address列的前12个字符放到了二级索引中,下边这个查询可能就有点儿尴尬了∶

SELECT * FROM shop ORDER BY address LIMIT 12;
sql
SELECT * FROM shop ORDER BY address LIMIT 12;

因为二级索引中不包含完整的address列信息,所以无法对前12个字符相同,后边的字符不同的记录进行排序,也就是使用索引列前缀的方式无法支持使用索引排序,只能使用文件排序。

拓展:Alibaba《Java开发手册》

【 强制 】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本
区分度决定索引长度。

说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会 高达 90% 以上 ,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。

区分度高(散列性高)的列适合作为索引

列的基数指的是某一列中不重复数据的个数,比方说某个列包含值2,5,8,2,5,8,2,5,8,虽然有
9条记录,但该列的基数却是3。也就是说,在记录行数一定的情况下,列的基数越大,该列中的值越分散;列的基数越小,该列中的值越集中。这个列的基数指标非常重要,直接影响我们是否能有效的利用索引。最好为列的基数大的列建立索引,为基数太小列的建立索引效果可能不好。

可以使用公式 select count(distinct a)/count(*) from t1 计算区分度,越接近1越好,一般超过33%就算是比较高效的索引了。

拓展:联合索引把区分度高(散列性高)的列放在前面。

使用最频繁的列放到联合索引的左侧

这样也可以较少的建立一些索引。同时,由于"最左前缀原则",可以增加联合索引的使用率。

在多个字段都要创建索引的情况下,联合索引优于单值索引

3.3 限制索引的数目
在实际工作中,我们也需要注意平衡,索引的数目不是越多越好。我们需要限制每张表上的索引数量,建议单张表索引数量不超过6个。原因:

1.每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。

2.索引会影响INSERT、DELETE、UPDATE等语句的性能,因为表中的数据更改的同时,索引也会进行调整和更新,会造成负担。

3.优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,会增加MysQL优化器生成执行计划时间,降低查询性能。

哪些情况不适合创建索引

在where中使用不到的字段,不要设置索引

WHERE条件((包括GROUP BY、ORDER BY)里用不到的字段不需要创建索引,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的。举个例子:

SELECT course_id, student_id, create_time FROM student_info
WHERE student_id = 41251;
sql
SELECT course_id, student_id, create_time FROM student_info
WHERE student_id = 41251;

因为我们是按照student_id 来进行检索的,所以不需要对其他字段创建索引,即使这些字段出现在SELECT字段中。

  1. 数据量小的表最好不要使用索引
    如果表记录太少,比如少于1000个,那么是不需要创建索引的。表记录太少,是否创建索引对查询效率的影响并不大。甚至说,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果。

举例:创建表1:

CREATE TABLE t_without_index(
a INT PRIMARY KEY AUTO_INCREMENT,
b INT
);
sql
CREATE TABLE t_without_index(
a INT PRIMARY KEY AUTO_INCREMENT,
b INT
);

提供存储过程1:

#创建存储过程

DELIMITER //
CREATE PROCEDURE t_wout_insert()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 900
DO
INSERT INTO t_without_index(b) SELECT RAND()*10000;
SET i = i + 1;
END WHILE;
COMMIT;
END //
DELIMITER ;

#调用
CALL t_wout_insert();
sql
#创建存储过程

DELIMITER //
CREATE PROCEDURE t_wout_insert()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 900
DO
INSERT INTO t_without_index(b) SELECT RAND()*10000;
SET i = i + 1;
END WHILE;
COMMIT;
END //
DELIMITER ;

#调用
CALL t_wout_insert();

创建表2:

CREATE TABLE t_with_index(
a INT PRIMARY KEY AUTO_INCREMENT,
b INT,
INDEX idx_b(b)
);
sql
CREATE TABLE t_with_index(
a INT PRIMARY KEY AUTO_INCREMENT,
b INT,
INDEX idx_b(b)
);

创建存储过程2:

#创建存储过程

DELIMITER //
CREATE PROCEDURE t_with_insert()

BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 900
DO
INSERT INTO t_with_index(b) SELECT RAND()*10000;
SET i = i + 1;
END WHILE;
COMMIT;
END //
DELIMITER ;

#调用
CALL t_with_insert();
sql
#创建存储过程

DELIMITER //
CREATE PROCEDURE t_with_insert()

BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 900
DO
INSERT INTO t_with_index(b) SELECT RAND()*10000;
SET i = i + 1;
END WHILE;
COMMIT;
END //
DELIMITER ;

#调用
CALL t_with_insert();

查询对比:

mysql> select * from t_without_index where b = 9879;
±-----±-----+
| a | b |
±-----±-----+
| 1242 | 9879 |
±-----±-----+
1 row in set (0.00 sec)
mysql> select * from t_with_index where b = 9879;
±----±-----+
| a | b |
±----±-----+
| 112 | 9879 |
±----±-----+
1 row in set (0.00 sec)
sql
mysql> select * from t_without_index where b = 9879;
±-----±-----+
| a | b |
±-----±-----+
| 1242 | 9879 |
±-----±-----+
1 row in set (0.00 sec)
mysql> select * from t_with_index where b = 9879;
±----±-----+
| a | b |
±----±-----+
| 112 | 9879 |
±----±-----+
1 row in set (0.00 sec)

你能看到运行结果相同,但是在数据量不大的情况下,索引就发挥不出作用了。

结论:在数据表中的数据行数比较少的情况下,比如不到 1000 行,是不需要创建索引的。

有大量重复数据的列上不要建立索引

在条件表达式中经常用到的不同值较多的列上建立索引,但字段中如果有大量重复数据,也不用创建索引。比如在学生表的“性别"字段上只有“男"与“女"两个不同值,因此无须建立索引。如果建立索引,不但不会提高查询效率,反而会严重降低数据更新速度。

举例1:要在 100 万行数据中查找其中的 50 万行(比如性别为男的数据),一旦创建了索引,你需要先
访问 50 万次索引,然后再访问 50 万次数据表,这样加起来的开销比不使用索引可能还要大。

举例2:假设有一个学生表,学生总数为 100 万人,男性只有 10 个人,也就是占总人口的 10 万分之 1。

学生表 student_gender 结构如下。其中数据表中的 student_gender 字段取值为 0 或 1,0 代表女性,1 代
表男性。

CREATE TABLE student_gender(
student_id INT(11) NOT NULL,
student_name VARCHAR(50) NOT NULL,
student_gender TINYINT(1) NOT NULL,
PRIMARY KEY(student_id)
)ENGINE = INNODB;
sql
CREATE TABLE student_gender(
student_id INT(11) NOT NULL,
student_name VARCHAR(50) NOT NULL,
student_gender TINYINT(1) NOT NULL,
PRIMARY KEY(student_id)
)ENGINE = INNODB;

如果我们要筛选出这个学生表中的男性,可以使用:

SELECT * FROM student_gender WHERE student_gender = 1
sql
SELECT * FROM student_gender WHERE student_gender = 1

运行结果(10 条数据,运行时间 0.696s ):

图片.png

你能看到在未创建索引的情况下,运行的效率并不高。如果针对student_gender字段创建索引呢?

SELECT * FROM student_gender WHERE student_gender = 1
sql
SELECT * FROM student_gender WHERE student_gender = 1

同样是10条数据,运行结果相同,时间却缩短到了0.052s,大幅提升了查询的效率。

其实通过这两个实验你也能看出来,索引的价值是帮你快速定位。如果想要定位的数据有很多,那么索引就失去了它的使用价值,比如通常情况下的性别字段。

结论:当数据重复度大,比如 高于 10% 的时候,也不需要对这个字段使用索引。

避免对经常更新的表创建过多的索引

第一层含义︰频繁更新的字段不一定要创建索引。因为更新数据的时候,也需要更新索引,如果索引太多,在更新索引的时候也会造成负担,从而影响效率。

第二层含义:避免对经常更新的表创建过多的索引,并且索引中的列尽可能少。此时,虽然提高了查询速度
同时却会降低更新表的速度。

不建议用无序的值作为索引

例如身份证、UUID(在索引比较时需要转为ASCII,并且插入时可能造成页分裂)、MD5、HASH、无序长字
符串等。

删除不再使用或者很少使用的索引

表中的数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再需要。数据库管理员应当定期找出这些索引,将它们删除,从而减少索引对更新操作的影响。

不要定义冗余或重复的索引

① 冗余索引

举例:建表语句如下

CREATE TABLE person_info(
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
birthday DATE NOT NULL,
phone_number CHAR(11) NOT NULL,
country varchar(100) NOT NULL,
PRIMARY KEY (id),
KEY idx_name_birthday_phone_number (name(10), birthday, phone_number),
KEY idx_name (name(10))
);
sql
CREATE TABLE person_info(
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
birthday DATE NOT NULL,
phone_number CHAR(11) NOT NULL,
country varchar(100) NOT NULL,
PRIMARY KEY (id),
KEY idx_name_birthday_phone_number (name(10), birthday, phone_number),
KEY idx_name (name(10))
);

我们知道,通过 idx_name_birthday_phone_number 索引就可以对 name 列进行快速搜索,再创建一
个专门针对 name 列的索引就算是一个 冗余索引 ,维护这个索引只会增加维护的成本,并不会对搜索有
什么好处。

② 重复索引

另一种情况,我们可能会对某个列 重复建立索引 ,比方说这样:

CREATE TABLE repeat_index_demo (
col1 INT PRIMARY KEY,
col2 INT,
UNIQUE uk_idx_c1 (col1),
INDEX idx_c1 (col1)
);
sql
CREATE TABLE repeat_index_demo (
col1 INT PRIMARY KEY,
col2 INT,
UNIQUE uk_idx_c1 (col1),
INDEX idx_c1 (col1)
);

我们看到,col1 既是主键、又给它定义为一个唯一索引,还给它定义了一个普通索引,可是主键本身就
会生成聚簇索引,所以定义的唯一索引和普通索引是重复的,这种情况要避免。

3.5小结
索引是一把双刃剑,可提高查询效率,但也会降低插入和更新的速度并占用磁盘空间。

选择索引的最终目的是为了使查询的速度变快,上面给出的原则是最基本的准则,但不能拘泥于上面的准则,大家要在以后的学习和工作中进行不断的实践,根据应用的实际情况进行分析和判断,选择最合适的索引方式。

在数据库调优中,我们的目标就是响应时间更快,吞吐量更大。利用宏观的监控工具和微观的日志分析可以帮我们快速找到调优的思路和方式。

性能分析工具的使用

  • 数据库服务器的优化步骤

通过观察了解数据库整体的运行状态,通过性能分析工具可以让我们了解执行慢的SQL都有哪些,查看具体的SQL执行计划,甚至是sQL执行中的每一步的成本代价,这样才能定位问题所在,找到了问题,再采取相应的行动。

  • 需要观察服务器的状态是否存在周期性的波动。如果存在周期性波动,有可能是周期性节点的原因,比如双十一、促销活动等。这样的话,我们可以通过A1这一步骤解决,也就是加缓存,或者更改缓存失效策略。
  • 如果缓存策略没有解决,或者不是周期性波动的原因,我们就需要进一步分析查询延迟和卡顿的原因。接下来进入S2这一步,我们需要开启慢查询。慢查询可以帮我们定位执行慢的SQL语句。我们可以通过设置long_query_time参数定义“慢"的阈值,如果SQL执行时间超过了long_query_time,则会认为是慢查询。当收集上来这些慢查询之后,我们就可以通过分析工具对慢查询日志进行分析。
  • 知道了执行慢的SQL,这样就可以针对性地用EXPLAIN查看对应SQL语句的执行计划,或者使用show profile查看SQL中每一个步骤的时间成本。这样我们就可以了解SQL查询慢是因为执行时间长,还是等待时间长。
  • 如果是SQL等待时间长,我们可以调优服务器的参数,比如适当增加数据库缓冲池等。
  • 如果是SQL执行时间长,我们需要考虑是索引设计的问题?还是查询关联的数据表过多?还是因为数据表的字段设计问题导致了这一现象。然后在这些维度上进行对应的调整。
  • 如果都不能解决问题,我们需要考虑数据库自身的SQL查询性能是否已经达到了瓶颈,如果确认没有达到性能瓶颈,就需要重新检查,重复以上的步骤。
  • 如果已经达到了性能瓶颈,进入A4阶段,需要考虑增加服务器,采用读写分离的架构,或者考虑对数据库进行分库分表,比如垂直分库、垂直分表和水平分表等。

以上就是数据库调优的流程思路。如果我们发现执行SQL时存在不规则延迟或卡顿的时候,就可以采用分析工具帮我们定位有问题的SQL,这三种分析工具你可以理解是SQL调优的三个步骤:慢查询、EXPLAIN和SHOW PROFILING。

查看系统性能参数

在MySQL中,可以使用 SHOW STATUS 语句查询一些MySQL数据库服务器的 性能参数 、 执行频率 。

SHOW STATUS语句语法如下:

SHOW [GLOBAL|SESSION] STATUS LIKE ‘参数’;

一些常用的性能参数如下:
• Connections:连接MySQL服务器的次数。
• Uptime:MySQL服务器的上线时间。
• Slow_queries:慢查询的次数。
• Innodb_rows_read:Select查询返回的行数
• Innodb_rows_inserted:执行INSERT操作插入的行数
• Innodb_rows_updated:执行UPDATE操作更新的行数
• Innodb_rows_deleted:执行DELETE操作删除的行数 • Com_select:查询操作的次数。
• Com_insert:插入操作的次数。对于批量插入的 INSERT 操作,只累加一次。
• Com_update:更新操作的次数。
• Com_delete:删除操作的次数。

统计SQL的查询成本:last_query_cost

一条SQL查询语句在执行前需要确定查询执行计划,如果存在多种执行计划的话,MysQL会计算每个执行计划所需要的成本,从中选择成本最小的一个作为最终执行的执行计划。

如果我们想要查看某条SQL语句的查询成本,可以在执行完这条SQL语句之后,通过查看当前会话中的
last_query_cost变量值来得到当前查询的成本。它通常也是我们评价一个查询的执行效率的一个常用指标。这个查询成本对应的是SQL语句所需要读取的页的数量。

我们依然使用第8章的 student_info 表为例:

CREATE TABLE `student_info` ( 
	`id` INT(11) NOT NULL AUTO_INCREMENT, 
	`student_id` INT NOT NULL , 
	`name` VARCHAR(20) DEFAULT NULL, 
	`course_id` INT NOT NULL , 
	`class_id` INT(11) DEFAULT NULL, 
	`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 
	PRIMARY KEY (`id`) 
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

如果我们想要查询 id=900001 的记录,然后看下查询成本,我们可以直接在聚簇索引上进行查找:

SELECT student_id, class_id, NAME, create_time FROM student_infoWHERE id = 900001;

运行结果(1 条记录,运行时间为 0.042s )

然后再看下查询优化器的成本,实际上我们只需要检索一个页即可:
SHOW STATUS LIKE 'last_query_cost';

如果我们想要查询 id 在 900001 到 9000100 之间的学生记录呢?

SELECT student_id, class_id, NAME, create_time FROM student_info WHERE id BETWEEN 900001 AND 900100;

运行结果(100 条记录,运行时间为 0.046s ):

然后再看下查询优化器的成本,这时我们大概需要进行 20 个页的查询。

SHOW STATUS LIKE 'last_query_cost';

你能看到页的数量是刚才的 20 倍,但是查询的效率并没有明显的变化,实际上这两个 SQL 查询的时间
基本上一样,就是因为采用了顺序读取的方式将页面一次性加载到缓冲池中,然后再进行查找。虽然 页 数量(last_query_cost)增加了不少 ,但是通过缓冲池的机制,并 没有增加多少查询时间 。

使用场景:它对于比较开销是非常有用的,特别是我们有好几种查询方式可选的时候。

SQL查询是一个动态的过程,从页加载的角度来看,我们可以得到以下两点结论:

位置决定效率。如果页就在数据库缓冲池中,那么效率是最高的,否则还需要从内存或者磁盘中进行读取,当然针对单个页的读取来说,如果页存在于内存中,会比在磁盘中读取效率高很多。
批量决定效率。如果我们从磁盘中对单一页进行随机读,那么效率是很低的(差不多10ms),而采用顺序读取的方式,批量对页进行读取,平均一页的读取效率就会提升很多,甚至要快于单个页面在内存中的随机读取。
所以说,遇到I/O并不用担心,方法找对了,效率还是很高的。我们首先要考虑数据存放的位置,如果是经常使用的数据就要尽量放到缓冲池中,其次我们可以充分利用磁盘的吞吐能力,一次性批量读取数据,这样单个页的读取效率也就得到了提升。

定位执行慢的 SQL:慢查询日志

MySQL的慢查询日志,用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是运行10秒以上(不含10秒)的语句,认为是超出了我们的最大忍耐时间值。

它的主要作用是,帮助我们发现那些执行时间特别长的SQL查询,并且有针对性地进行优化,从而提高系统的整体效率。当我们的数据库服务器发生阻塞、运行变慢的时候,检查一下慢查询日志,找到那些慢查询,对解决问题很有帮助。比如一条sql执行超过5秒钟,我们就算慢SQL,希望能收集超过5秒的sql,结合explain进行全面分析。

默认情况下,MySQL数据库没有开启慢查询日志,需要我们手动来设置这个参数。如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。

慢查询日志支持将日志记录写入文件。

开启慢查询日志参数

# 开启slow_query_log
show variables like '%slow_query_log';
set global slow_query_log= ON;

# 设置慢查询的时间阈值
set global long_query_time = 1; 
show global variables like '%long_query_time%'; 
set long_query_time=1; 
show variables like '%long_query_time%';

# 配置文件中一并设置参数
# 修改my.cnf文件,[mysqld]下增加或修改参数long_query_time、slow_query_log和slow_query_log_file后,然后重启MySQL服务器。
# 如果不指定存储路径,慢查询日志将默认存储到MySQL数据库的数据文件夹下。如果不指定文件名,默认文件名为hostname-slow.log。

查看慢查询数目

# 查询当前系统中有多少条慢查询记录
SHOW GLOBAL STATUS LIKE '%Slow_queries%';

案例

  • 建表
CREATE TABLE `student` ( 
	`id` INT(11) NOT NULL AUTO_INCREMENT, 
	`stuno` INT NOT NULL , 
	`name` VARCHAR(20) DEFAULT NULL, 
	`age` INT(3) DEFAULT NULL, 
	`classId` INT(11) DEFAULT NULL, PRIMARY KEY (`id`) 
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
  • 设置参数 log_bin_trust_function_creators

创建函数,假如报错:This function has none of DETERMINISTIC…

# 允许创建函数设置
set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。
  • 创建函数
# 随机产生字符串
DELIMITER // 
CREATE FUNCTION rand_string(n INT) 
	RETURNS VARCHAR(255) #该函数会返回一个字符串 
BEGIN
	DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ'; 
	DECLARE return_str VARCHAR(255) DEFAULT ''; 
	DECLARE i INT DEFAULT 0; 
	WHILE i < n DO SET 
	return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1)); 
	SET i = i + 1; 
		END WHILE; 
		RETURN return_str; 
END // 
DELIMITER ; 
#测试 
SELECT rand_string(10);


# 产生随机数值
DELIMITER // 
CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11) 
BEGIN 
	DECLARE i INT DEFAULT 0; 
	SET i = FLOOR(from_num +RAND()*(to_num - from_num+1)) ; 
	RETURN i; 
END // 
DELIMITER ; 
#测试: 
SELECT rand_num(10,100);
  • 创建存储过程
DELIMITER // 
CREATE PROCEDURE insert_stu1( START INT , max_num INT ) 
BEGIN 
	DECLARE i INT DEFAULT 0; 
		SET autocommit = 0; #设置手动提交事务 
		REPEAT #循环 
	SET i = i + 1; #赋值 
	INSERT INTO student (stuno, NAME ,age ,classId ) VALUES ((START+i),rand_string(6),rand_num(10,100),rand_num(10,1000)); 
	UNTIL i = max_num END REPEAT; 
	COMMIT; #提交事务 
END // 
DELIMITER ;
  • 调用存储过程
#调用刚刚写好的函数, 4000000条记录,从100001号开始 
CALL insert_stu1(100001,4000000);
  • 测试
    SELECT * FROM student WHERE stuno = 3455655;
    SELECT * FROM student WHERE name = 'oQmLUr';

从上面的结果可以看出来,查询学生编号为“3455655”的学生信息花费时间为2.09秒。查询学生姓名为
“oQmLUr”的学生信息花费时间为2.39秒。已经达到了秒的数量级,说明目前查询效率是比较低的,下面
的小节我们分析一下原因。

  • 分析
    show status like 'slow_queries';

补充说明:
除了上述变量,控制慢查询日志的还有一个系统变量: min_examined_row_limit。这个变量的意思是,查询扫描过的最少记录数。这个变量和查询执行时间,共同组成了判别一个查询是否是慢查询的条件。如果查询扫描过的记录数大于等于这个变量的值,并且查询执行时间超过long_query_time的值,那么,这个查询就被记录到慢查询日志中;反之,则不被记录到慢查询日志中。
图片.png
这个值默认是0。与long_query_time=10合在一起,表示只要查询的执行时间超过10秒钟,哪怕一个记录也没有扫描过,都要被记录到慢查询日志中。你也可以根据需要,通过修改“my.ini"文件,来修改查询时长,或者通过SET指令,用SQL语句修改“min_examined_row_limit”的值。

慢查询日志分析工具:mysqldumpslow

在生产环境中,如果要手工分析日志,查找、分析SQL,显然是个体力活,MySQL提供了日志分析工具
mysqldumpslow 。

查看mysqldumpslow的帮助信息
mysqldumpslow --help

mysqldumpslow 命令的具体参数如下:

a: 不将数字抽象成N,字符串抽象成S
s: 是表示按照何种方式排序:
c: 访问次数
l: 锁定时间
r: 返回记录
t: 查询时间
al:平均锁定时间
ar:平均返回记录数
at:平均查询时间 (默认方式)
ac:平均查询次数
-t: 即为返回前面多少条的数据;
-g: 后边搭配一个正则匹配模式,大小写不敏感的;
举例:我们想要按照查询时间排序,查看前五条 SQL 语句,这样写即可:

mysqldumpslow -s t -t 5 /var/lib/mysql/atguigu01-slow.log

工作常用参考:

#得到返回记录集最多的10个SQL 
mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log 

#得到访问次数最多的10个SQL 
mysqldumpslow -s c -t 10 /var/lib/mysql/atguigu-slow.log 

#得到按照时间排序的前10条里面含有左连接的查询语句 
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/atguigu-slow.log 

#另外建议在使用这些命令时结合 | 和more 使用 ,否则有可能出现爆屏情况 
mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log | more

关闭慢查询日志

MySQL服务器停止慢查询日志功能有两种方法:

永久性方式

修改my.cnf或者my.ini文件,把[mysqld]组下的slow_query_log值设置为OFF,修改保存后,再重启MysQL服务,即可生效;

[mysqld] slow_query_log=OFF

或者,把slow_query_log一项注释掉 或 删除

[mysqld] 
#slow_query_log =OFF

重启MySQL服务,执行如下语句查询慢日志功能。

SHOW VARIABLES LIKE '%slow%'; #查询慢查询日志所在目录 
SHOW VARIABLES LIKE '%long_query_time%'; #查询超时时长

可以看到,MySQL系统中的慢查询日志是关闭的。

临时性方式

使用SET语句来设置。

(1)停止MySQL慢查询日志功能,具体SQL语句如下。

SET GLOBAL slow_query_log=off;

(2)重启MySQL服务,使用SHOW语句查询慢查询日志功能信息,具体SQL语句如下

SHOW VARIABLES LIKE '%slow%'; 
# 以及 
SHOW VARIABLES LIKE '%long_query_time%';

删除慢查询日志

使用SHOW语句显示慢查询日志信息,具体sQL语句如下。

SHow VARIABLES LIKE 'slow_query_log%';

从执行结果可以看出,慢查询日志的目录默认为MySQL的数据目录,在该目录下手动删除慢查询日志文件即可。使用命令mysqladmin flush-logs来重新生成查询日志文件,具体命令如下,执行完毕会在数据目录下重新生成慢查询日志文件。

mysqladmin -uroot -p flush-logs slow

提示
慢查询日志都是使用mysqladmin flush-logs命令来删除重建的。使用时一定要注意,一旦执行了这个命令,慢查询日志都只存在新的日志文件中,如果需要旧的查询日志,就必须事先备份。

查看 SQL 执行成本:SHOW PROFILE

show profile在《逻辑架构》章节中讲过,这里作为复习。

Show profile是MysQL提供的可以用来分析当前会话中SQL都做了什么、执行的资源消耗情况的工具,可用于sql调优的测量。默认情况下处于关闭状态,并保存最近15次的运行结果。

我们可以在会话级别开启这个功能

show variables like 'profiling';

通过设置 profiling='ON’ 来开启 show profile:

set profiling = 'ON';

然后执行相关的查询语句。接着看下当前会话都有哪些 profiles,使用下面这条命令:

show profiles;

你能看到当前会话一共有 2 个查询。如果我们想要查看最近一次查询的开销,可以使用:

show profile;

我们也可以查看指定的Query lD的开销,比如show profile for query 2查询结果是一样的。在SHOw PROFILE中我们可以查看不同部分的开销,比如cpu、block.io等:

show profile cpu,block io for query 2;

show profile的常用查询参数:

① ALL:显示所有的开销信息。 ② BLOCK IO:显示块IO开销。 ③ CONTEXT SWITCHES:上下文切换开
销。 ④ CPU:显示CPU开销信息。 ⑤ IPC:显示发送和接收开销信息。 ⑥ MEMORY:显示内存开销信
息。 ⑦ PAGE FAULTS:显示页面错误开销信息。 ⑧ SOURCE:显示和Source_function,Source_file, Source_line相关的开销信息。 ⑨ SWAPS:显示交换次数开销信息。

日常开发需注意的结论:

converting HEAP to MyISAM:查询结果太大,内存不够,数据往磁盘上搬了。
Creating Itmp table:创建临时表。先拷贝数据到临时表,用完后再删除临时表。
copying to tmp table on disk:把内存中临时表复制到磁盘上,警惕!
locked。
如果在show profile诊断结果中出现了以上4条结果中的任何一条,则sql语句需要优化。
注意:

不过SHOW PROFILE命令将被弃用,我们可以从information_schema中的profiling数据表进行查看。

分析查询语句:EXPLAIN

6.1 概述
定位了查询慢的SQL之后,我们就可以使用EXPLAIN或DESCRIBE工具做针对性的分析查询语句。DESCRIBE语句的使用方法与EXPLAIN语句是一样的,并且分析结果也是一样的。

MySQL中有专门负责优化SELECT语句的优化器模块,主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的Query提供它认为最优的执行计划(他认为最优的数据检索方式,但不见得是DBA认为是最优的,这部分最耗费时间)。

这个执行计划展示了接下来具体执行查询的方式,比如多表连接的顺序是什么,对于每个表采用什么访问方法来具体执行查询等等。MySQL为我们提供了EXPLAIN语句来帮助我们查看某个查询语句的具体执行计划,大家看懂EXPLAIN语句的各个输出项,可以有针对性的提升我们查询语句的性能。

  1. 能做什么?
    表的读取顺序
    数据读取操作的操作类型
    哪些索引可以使用
    哪些索引被实际使用
    表之间的引用
    每张表有多少行被优化器查询
  2. 官网介绍
    https://dev.mysql.com/doc/refman/5.7/en/explain-output.html

https://dev.mysql.com/doc/refman/8.0/en/explain-output.html

图片.png

  1. 版本情况
    MySQL 5.6.3以前只能 EXPLAIN SELECT ;MYSQL 5.6.3以后就可以 EXPLAIN SELECT,UPDATE, DELETE 在
    5.7以前的版本中,想要显示 partitions 需要使用 explain partitions 命令;想要显示filtered 需要使用 explain extended 命令。在5.7版本后,默认explain直接显示partitions和 filtered中的信息。
    图片.png

6.2 基本语法
EXPLAIN 或 DESCRIBE语句的语法形式如下:

EXPLAIN SELECT select_options
或者
DESCRIBE SELECT select_options

如果我们想看看某个查询的执行计划的话,可以在具体的查询语句前边加一个 EXPLAIN ,就像这样:

mysql> EXPLAIN SELECT 1;

输出的上述信息就是所谓的执行计划。在这个执行计划的辅助下,我们需要知道应该怎样改进自己的查询语句以使查询执行起来更高效。其实除了以SELECT开头的查询语句,其余的DELETE、INSERT、REPLACE以及
UPDATE语句等都可以加上EXPLAIN,用来查看这些语句的执行计划,只是平时我们对SELECT语句更感兴趣。

注意:执行EXPLAIN时并没有真正的执行该后面的语句,因此可以安全的查看执行计划。

EXPLAIN 语句输出的各个列的作用如下:

在这里把它们都列出来只是为了描述一个轮廓,让大家有一个大致的印象。

6.3 数据准备

# 建表
CREATE TABLE s1 ( 
	id INT AUTO_INCREMENT, 
	key1 VARCHAR(100), 
	key2 INT, key3 VARCHAR(100), 
	key_part1 VARCHAR(100), 
	key_part2 VARCHAR(100), 
	key_part3 VARCHAR(100), 
	common_field VARCHAR(100), 
	PRIMARY KEY (id), 
	INDEX idx_key1 (key1), 
	UNIQUE INDEX idx_key2 (key2), 
	INDEX idx_key3 (key3), 
	INDEX idx_key_part(key_part1, key_part2, key_part3) 
) ENGINE=INNODB CHARSET=utf8;
CREATE TABLE s2 ( 
	id INT AUTO_INCREMENT, 
	key1 VARCHAR(100), 
	key2 INT, 
	key3 VARCHAR(100), 
	key_part1 VARCHAR(100), 
	key_part2 VARCHAR(100), 
	key_part3 VARCHAR(100), 
	common_field VARCHAR(100), 
	PRIMARY KEY (id), 
	INDEX idx_key1 (key1), 
	UNIQUE INDEX idx_key2 (key2),
	INDEX idx_key3 (key3), 
	INDEX idx_key_part(key_part1, 
	key_part2, key_part3) 
) ENGINE=INNODB CHARSET=utf8;

# 设置参数 log_bin_trust_function_creators
# 创建函数,假如报错,需开启如下命令:允许创建函数设置:
set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。


# 创建函数
DELIMITER // 
CREATE FUNCTION rand_string1(n INT) 
	RETURNS VARCHAR(255) #该函数会返回一个字符串 
BEGIN
	DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ'; 
	DECLARE return_str VARCHAR(255) DEFAULT ''; 
	DECLARE i INT DEFAULT 0; 
	WHILE i < n DO 
		SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1)); 	
		SET i = i + 1; 
	END WHILE; 
	RETURN return_str; 
END // 
DELIMITER ;

# 创建存储过程
## 创建往s1表中插入数据的存储过程:
DELIMITER // 
CREATE PROCEDURE insert_s1 (IN min_num INT (10),IN max_num INT (10)) 
BEGIN
	DECLARE i INT DEFAULT 0; 
	SET autocommit = 0; 
	REPEAT 
	SET i = i + 1; 
	INSERT INTO s1 VALUES( (min_num + i), 
	rand_string1(6), 
	(min_num + 30 * i + 5), 
	rand_string1(6), 
	rand_string1(10), 
	rand_string1(5), 
	rand_string1(10), 
	rand_string1(10)); 
	UNTIL i = max_num 
	END REPEAT; 
	COMMIT; 
END // 
DELIMITER ;

## 创建往s2表中插入数据的存储过程:
DELIMITER // 
CREATE PROCEDURE insert_s2 (IN min_num INT (10),IN max_num INT (10)) 
BEGIN
DECLARE i INT DEFAULT 0; 
SET autocommit = 0; 
REPEAT 
SET i = i + 1; 
INSERT INTO s2 VALUES( (min_num + i), 
	rand_string1(6), 
	(min_num + 30 * i + 5), 
	rand_string1(6), 
	rand_string1(10), 
	rand_string1(5), 
	rand_string1(10), 
	rand_string1(10)); 
	UNTIL i = max_num 
	END REPEAT; 
	COMMIT; 
END // 
DELIMITER ;


# 调用存储过程
## s1表数据的添加:加入1万条记录:
CALL insert_s1(10001,10000);
## s2表数据的添加:加入1万条记录:
CALL insert_s2(10001,10000);

EXPLAIN各列作用

为了让大家有比较好的体验,我们调整了下 EXPLAIN 输出列的顺序。

  1. table
    不论我们的查询语句有多复杂,里边儿 包含了多少个表 ,到最后也是需要对每个表进行 单表访问 的,所
    以MySQL规定EXPLAIN语句输出的每条记录都对应着某个单表的访问方法,该条记录的table列代表着该
    表的表名(有时不是真实的表名字,可能是简称)。

mysql> EXPLAIN SELECT * FROM s1;

图片.png

这个查询语句只涉及对s1表的单表查询,所以EXPLAIN输出中只有一条记录,其中的table列的值是s1,表明这条记录是用来说明对s1表的单表访问方法的。

下边我们看一个连接查询的执行计划:

mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2;

图片.png
可以看到这个连接查询的执行计划中有两条记录,这两条记录的table列分别是s1和s2,这两条记录用来分别说明对s1表和s2表的访问方法是什么。

  1. id
    我们写的查询语句一般都以 SELECT 关键字开头,比较简单的查询语句里只有一个 SELECT 关键字,比
    如下边这个查询语句:

SELECT * FROM s1 WHERE key1 = ‘a’;

稍微复杂一点的连接查询中也只有一个 SELECT 关键字,比如:

SELECT * FROM s1 INNER JOIN s2
ON s1.key1 = s2.key1
WHERE s1.common_field = ‘a’;

但是下边两种情况下在一条查询语句中会出现多个SELECT 关键字:

查询中包含子查询的情况

比如下边这个查询语句中就包含2个SELECT关键字:

SELECT * FROM s1
WHERE key1 IN ( SELECT key3 FROM s2);

查询中包含UNION语句的情况

比如下边这个查询语句中也包含2个SELECT关键字:

SELECT * FROM s1 UNION SELECT * FROM s2;

查询语句中每出现一个SELECT关键字,MySQL就会为它分配一个唯一的id值。这个id值就是EXPLAIN语句的第一个列,比如下边这个查询中只有一个SELECT关键字,所以EXPLAIN的结果中也就只有一条id列为1的记录:

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = ‘a’;

图片.png

对于连接查询来说,一个SELECT关键字后边的FROM子句中可以跟随多个表,所以在连接查询的执行计划中,每个表都会对应一条记录,但是这些记录的id值都是相同的,比如:

mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2;

图片.png

可以看到,上述连接查询中参与连接的s1和s2表分别对应一条记录,但是这两条记录对应的id值都是1。这里需要大家记住的是,在连接查询的执行计划中,每个表都会对应一条记录,这些记录的id列的值是相同的,出现在前边的表表示驱动表,出现在后边的表表示被驱动表。所以从上边的EXPLAIN输出中我们可以看出,查询优化器准备让s1表作为驱动表,让s2表作为被驱动表来执行查询。

对于包含子查询的查询语句来说,就可能涉及多个SELECT关键字,所以在包含子查询的查询语句的执行计划中,每个SELECT 关键字都会对应一个唯一的id值,比如这样:

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2) OR key3 = ‘a’;

图片.png

从输出结果中我们可以看到;s1表在外层查询中,外层查询有一个独立的SELECT关键字,所以第一条记录的
id值就是1,s2表在子查询中,子查询有一个独立的SELECT关键字,所以第二条记录的id值就是2。

但是这里大家需要特别注意,查询优化器可能对涉及子查询的查询语句进行重写,从而转换为连接查询。所以如果我们想知道查询优化器对某个包含子查询的语句是否进行了重写,直接查看执行计划就好了,比如说:

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key2 FROM s2 WHERE common_field = ‘a’);

图片.png

可以看到,虽然我们的查询语句是一个子查询,但是执行计划中s1和s2表对应的记录的id值全部是1,这就表明了查询优化器将子查询转换为了连接查询。

对于包含UNION子句的查询语句来说,每个SELECT关键字对应一个id值也是没错的,不过还是有点儿特别的东西,比方说下边这个查询:

mysql> EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2;

图片.png

这个语句的执行计划的第三条记录是什么?为何id值是NULL,而且table列也很奇怪?UNION!它会把多个查询的结果集合并起来并对结果集中的记录进行去重,怎么去重呢?MySQL使用的是内部的临时表。正如上边的查询计划中所示,UNION子句是为了把id为1的查询和id为2的查询的结果集合并起来并去重,所以在内部创建了一个名为<union1,2>的临时表(就是执行计划第三条记录的table列的名称),id为NULL表明这个临时表是为了合并两个查询的结果集而创建的。

跟UNION对比起来,UNION ALL就不需要为最终的结果集进行去重,它只是单纯的把多个查询的结果集中的记录合并成一个并返回给用户,所以也就不需要使用临时表。所以在包含UNION ALL子句的查询的执行计划中,就没有那个id为NULL的记录,如下所示:

mysql> EXPLAIN SELECT * FROM s1 UNION ALL SELECT * FROM s2;

图片.png

小结:

id如果相同,可以认为是一组,从上往下顺序执行
在所有组中,id值越大,优先级越高,越先执行
关注点:id号每个号码,表示一趟独立的查询, 一个sql的查询趟数越少越好
3. select_type
一条大的查询语句里边可以包含若干个SELECT关键字,每个SELECT关键字代表着一个小的查询语句,而每个SELECT关键字的FROM子句中都可以包含若干张表(这些表用来做连接查询),每一张表都对应着执行计划输出中的一条记录,对于在同一个SELECT关键字中的表来说,它们的id值是相同的。

MySQL为每一个SELECT关键字代表的小查询都定义了一个称之为select_type的属性,意思是我们只要知道了某个小查询的select_type属性,就知道了这个小查询在整个大查询中扮演了一个什么角色,我们看一下
select_type都能取哪些值,请看官方文档:

图片.png

具体分析如下:

SIMPLE
查询语句中不包含UNION或者子查询的查询都算作是SIMPLE 类型,比方说下边这个单表查询的select_type的值就是SIMPLE:

mysql> EXPLAIN SELECT * FROM s1;

图片.png

当然,连接查询也算是 SIMPLE 类型,比如:

mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2;

图片.png

PRIMARY
对于包含UNION、UNION ALL或者子查询的大查询来说,它是由几个小查询组成的,其中最左边的那个查询的select_type值就是PRIMARY,比方说:

mysql> EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2;

图片.png

从结果中可以看到,最左边的小查询SELECT * FROM s1对应的是执行计划中的第一条记录,它的select_type值就是 PRIMARY。

UNION
对于包含UNION或者UNIN ALL的大查询来说,它是由几个小查询组成的,其中除了最左边的那个小查询以外,其余的小查询的select_type值就是UNION,可以对比上一个例子的效果。

UNION RESULT
MySQL选择使用临时表来完成UNION查询的去重工作,针对该临时表的查询的select_type就是UNION RESULT,例子上边有。

SUBQUERY
如果包含子查询的查询语句不能够转为对应的semi-join的形式,并且该子查询是不相关子查询,并且查询优化器决定采用将该子查询物化的方案来执行该子查询时,该子查询的第一个SELECT关键字代表的那个查询的select_type就是 SUBQUERY,比如下边这个查询:

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2) OR key3 = ‘a’;

图片.png

DEPENDENT SUBQUERY
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2
WHERE s1.key2 = s2.key2) OR key3 = ‘a’;

图片.png

DEPENDENT UNION
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2 WHERE key1 = ‘a’
UNION SELECT key1 FROM s1 WHERE key1 = ‘b’);

图片.png

DERIVED
mysql> EXPLAIN SELECT * FROM (SELECT key1, count(*) as c FROM s1 GROUP BY key1)
AS derived_s1 where c > 1;

图片.png

MATERIALIZED
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2);

图片.png

UNCACHEABLE SUBQUERY

不常用,就不多说了。

UNCACHEABLE UNION

  1. partitions (可略)
    代表分区表中的命中情况,非分区表,该项为NULL。一般情况下我们的查询语句的执行计划的partition列的值都是NULL。

https://dev.mysql.com/doc/refman/5.7/en/alter-table-partition-operations.html

如果想详细了解,可以如下方式测试。创建分区表:

– 创建分区表,
– 按照id分区,id<100 p0分区,其他p1分区

CREATE TABLE user_partitions (
id INT auto_increment,
NAME VARCHAR(12),
PRIMARY KEY(id))
PARTITION BY RANGE(id)
( PARTITION p0 VALUES less than(100),
PARTITION p1 VALUES less than MAXVALUE );

图片.png

DESC SELECT * FROM user_partitions WHERE id>200;

查询id大于200(200>100,p1分区)的记录,查看执行计划,partitions是p1,符合我们的分区规则

图片.png

  1. type ☆
    执行计划的一条记录就代表着MySQL对某个表的执行查询时的访问方法,又称“访问类型”,其中的type列就表明了这个访问方法是啥,是较为重要的一个指标。比如,看到type列的值是ref,表明MysQL即将使用ref访问方法来执行对s1表的查询。

完整的访问方法如下: system , const , eq_ref , ref , fulltext , ref_or_null , index_merge , unique_subquery , index_subquery , range , index , ALL 。

我们详细解释一下:

system
当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如MyISAM、Memory,那么对该表的访问方法就是system。比方说我们新建一个MyISAM表,并为其插入一条记录:

mysql> CREATE TABLE t( i int) Engine=MyISAM;
Query OK, 0 rows affected (0.05 sec)

mysql> INSERT INTO t VALUES(1);
Query OK, 1 row affected (0.01 sec)
sql
mysql> CREATE TABLE t( i int) Engine=MyISAM;
Query OK, 0 rows affected (0.05 sec)

mysql> INSERT INTO t VALUES(1);
Query OK, 1 row affected (0.01 sec)

然后我们看一下查询这个表的执行计划:

mysql> EXPLAIN SELECT * FROM t;
sql
mysql> EXPLAIN SELECT * FROM t;

图片.png

可以看到type列的值就是system了。

测试:可以把表改成使用InnoDB存储引擎,试试看执行计划的type列是什么。ALL

const
当我们根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是const,比如:

mysql> EXPLAIN SELECT * FROM s1 WHERE id = 10005;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE id = 10005;

图片.png

eq_ref
在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较),则对该被驱动表的访问方法就是eq_ref,比方说:

mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id;
sql
mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id;

图片.png

从执行计划的结果中可以看出,MySQL打算将s2作为驱动表,s1作为被驱动表,重点关注s1的访问
方法是 eq_ref ,表明在访问s1表的时候可以 通过主键的等值匹配 来进行访问。

ref
当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是ref,比方说下边这个查询:

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = ‘a’;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = ‘a’;

图片.png

fulltext
全文索引

ref_or_null
当对普通二级索引进行等值匹配查询,该索引列的值也可以是NULL值时,那么对该表的访问方法就可能是ref_or_null,比如说:

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = ‘a’ OR key1 IS NULL;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = ‘a’ OR key1 IS NULL;

图片.png

index_merge
一般情况下对于某个表的查询只能使用到一个索引,但单表访问方法时在某些场景下可以使用
Intersection、Union、Sort-Union这三种索引合并的方式来执行查询。我们看一下执行计划中是怎么体现MySQL使用索引合并的方式来对某个表执行查询的:

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = ‘a’ OR key3 = ‘a’;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = ‘a’ OR key3 = ‘a’;

图片.png

从执行计划的 type 列的值是 index_merge 就可以看出,MySQL 打算使用索引合并的方式来执行
对 s1 表的查询。

unique_subquery
类似于两表连接中被驱动表的eq_ref访问方法,unique_subquery是针对在一些包含IN子查询的查询语句中,如果查询优化器决定将IN子查询转换为EXISTS子查询,而且子查询可以使用到主键进行等值匹配的话,那么该子查询执行计划的type列的值就是unique_subquery,比如下边的这个查询语句:

mysql> EXPLAIN SELECT * FROM s1 WHERE key2 IN (SELECT id FROM s2 where s1.key1 = s2.key1)
OR key3 = ‘a’;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key2 IN (SELECT id FROM s2 where s1.key1 = s2.key1)
OR key3 = ‘a’;

图片.png

可以看到执行计划的第二条记录的type值就是unique_subquery,说明在执行子查询时会使用到id列的索引。

index_subquery
index_subquery与unique_subquery类似,只不过访问子查询中的表时使用的是普通的索引,比如这样:

mysql> EXPLAIN SELECT * FROM s1 WHERE common_field IN (SELECT key3 FROM s2 where
s1.key1 = s2.key1) OR key3 = ‘a’;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE common_field IN (SELECT key3 FROM s2 where
s1.key1 = s2.key1) OR key3 = ‘a’;

图片.png

range
如果使用索引获取某些范围区间的记录,那么就可能使用到range访问方法,比如下边的这个查询:

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (‘a’, ‘b’, ‘c’);
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (‘a’, ‘b’, ‘c’);

图片.png

或者:

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > ‘a’ AND key1 < ‘b’;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > ‘a’ AND key1 < ‘b’;

图片.png

index
当我们可以使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是index,比如这样:

mysql> EXPLAIN SELECT key_part2 FROM s1 WHERE key_part3 = ‘a’;
sql
mysql> EXPLAIN SELECT key_part2 FROM s1 WHERE key_part3 = ‘a’;

图片.png

上述查询中的搜索列表中只有key_part2一个列,而且搜索条件中也只有key_part3一个列,这两个列又恰好包含在idx_key_part这个索引中,可是搜索条件key_part3不能直接使用该索引进行ref或者range方式的访问,只能扫描整个idx_key_part索引的记录,所以查询计划的type列的值就是index。

再一次强调,对于使用InnoDB存储引擎的表来说,二级索引的记录只包含索引列和主键列的值,而聚簇索引中包含用户定义的全部列以及一些隐藏列,所以扫描二级索引的代价比直接全表扫描,也就是扫描聚簇索引的代价更低一些。

ALL
最熟悉的全表扫描,就不多说了,直接看例子:

mysql> EXPLAIN SELECT * FROM s1;
sql
mysql> EXPLAIN SELECT * FROM s1;

图片.png

一般来说,这些访问方法中除了All这个访问方法外,其余的访问方法都能用到索引,除了index_merge访问方法外,其余的访问方法都最多只能用到一个索引。

图片.png

  1. possible_keys和key
    在EXPLAIN语句输出的执行计划中,possible_keys列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些。一般查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用。key列表示实际用到的索引有哪些,如果为NULL,则没有使用索引。比方说下边这个查询:

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > ‘z’ AND key3 = ‘a’;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > ‘z’ AND key3 = ‘a’;

图片.png

  1. key_len ☆
    EXPLAIN SELECT * FROM s1 WHERE id = 10005;
    sql
    EXPLAIN SELECT * FROM s1 WHERE id = 10005;

图片.png

mysql> EXPLAIN SELECT * FROM s1 WHERE key2 = 10126;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key2 = 10126;

图片.png

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = ‘a’;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = ‘a’;

图片.png

mysql> EXPLAIN SELECT * FROM s1 WHERE key_part1 = ‘a’;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key_part1 = ‘a’;

图片.png

mysql> EXPLAIN SELECT * FROM s1 WHERE key_part1 = ‘a’ AND key_part2 = ‘b’;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key_part1 = ‘a’ AND key_part2 = ‘b’;

图片.png

练习:
key_len的长度计算公式:

varchar(10)变长字段且允许NULL = 10 * ( character set:
utf8=3,gbk=2,latin1=1)+1(NULL)+2(变长字段)

varchar(10)变长字段且不允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)+2(变长字段)

char(10)固定字段且允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)+1(NULL)

char(10)固定字段且不允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)
bash
varchar(10)变长字段且允许NULL = 10 * ( character set:
utf8=3,gbk=2,latin1=1)+1(NULL)+2(变长字段)

varchar(10)变长字段且不允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)+2(变长字段)

char(10)固定字段且允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)+1(NULL)

char(10)固定字段且不允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)

  1. ref
    mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = ‘a’;
    sql
    mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = ‘a’;

图片.png

mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id;
sql
mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id;

图片.png

mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s2.key1 = UPPER(s1.key1);
sql
mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s2.key1 = UPPER(s1.key1);

图片.png

  1. rows ☆
    预估的需要读取的记录条数值越小越好

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > ‘z’;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > ‘z’;

图片.png

  1. filtered
    某个表经过搜索条件过滤后剩余记录条数的百分比

如果使用的是索引执行的单表扫描,那么计算时需要估计出满足除使用到对应索引的搜索条件外的其他搜索条件的记录有多少条。

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > ‘z’ AND common_field = ‘a’;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > ‘z’ AND common_field = ‘a’;

图片.png

对于单表查询来说,这个filtered列的值没什么意义,我们更关注在连接查询中驱动表对应的执行计划记录的filtered值,它决定了被驱动表要执行的次数(即: rows * filtered)

mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key1
WHERE s1.common_field = ‘a’;
sql
mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key1
WHERE s1.common_field = ‘a’;

图片.png

  1. Extra ☆
    顾名思义,Extra列是用来说明一些额外信息的,包含不适合在其他列中显示但十分重要的额外信息。我们可以通过这些额外信息来更准确的理解MySQL到底将如何执行给定的查询语句。MySQL提供的额外信息有好几十个,我们就不一个一个介绍了,所以我们只挑比较重要的额外信息介绍给大家。

No tables used
当查询语句的没有FROM子句时将会提示该额外信息,比如:
mysql> EXPLAIN SELECT 1;
sql
mysql> EXPLAIN SELECT 1;

图片.png

Impossible WHERE
查询语句的WHERE子句永远为FALSE时将会提示该额外信息

mysql> EXPLAIN SELECT * FROM s1 WHERE 1 != 1;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE 1 != 1;

图片.png

Using where
当我们使用全表扫描来执行对某个表的查询,并且该语句的WHERE子句中有针对该表的搜索条件时,在Extra列中会提示上述额外信息。

mysql> EXPLAIN SELECT * FROM s1 WHERE common_field = ‘a’;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE common_field = ‘a’;

图片.png

当使用索引访问来执行对某个表的查询,并且该语句的WHERE子句中有除了该索引包含的列之外的其他搜索条件时,在`Extra '列中也会提示上述额外信息。

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = ‘a’ AND common_field = ‘a’;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = ‘a’ AND common_field = ‘a’;

图片.png

No matching min/max row
当查询列表处有MIN或者MAx聚合函数,但是并没有符合WHERE子句中的搜索条件的记录时,将会提示该额外信息

mysql> EXPLAIN SELECT MIN(key1) FROM s1 WHERE key1 = ‘abcdefg’;
sql
mysql> EXPLAIN SELECT MIN(key1) FROM s1 WHERE key1 = ‘abcdefg’;

图片.png

Using index
当我们的查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用覆盖索引的情况下,在Extra’列将会提示该额外信息。比方说下边这个查询中只需要用到idx key1而不需要回表操作:

mysql> EXPLAIN SELECT key1 FROM s1 WHERE key1 = ‘a’;
sql
mysql> EXPLAIN SELECT key1 FROM s1 WHERE key1 = ‘a’;

图片.png

Using index condition
有些搜索条件中虽然出现了索引列,但却不能使用到索引看课件理解索引条件下推

SELECT * FROM s1 WHERE key1 > ‘z’ AND key1 LIKE ‘%a’;
sql
SELECT * FROM s1 WHERE key1 > ‘z’ AND key1 LIKE ‘%a’;

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > ‘z’ AND key1 LIKE ‘%b’;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > ‘z’ AND key1 LIKE ‘%b’;

图片.png

Using join buffer (Block Nested Loop)
在连接查询执行过程中,当被驱动表不能有效的利用索引加快访问速度,MySQL一般会为其分配一块名叫join buffer的内存块来加快查询速度,也就是我们所讲的基于块的嵌套循环算法,比如下边这个查询语句:

mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.common_field = s2.common_field;
sql
mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.common_field = s2.common_field;

图片.png

图片.png

Not exists
图片.png

mysql> EXPLAIN SELECT * FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1 WHERE s2.id IS NULL;
sql
mysql> EXPLAIN SELECT * FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1 WHERE s2.id IS NULL;

图片.png

图片.png

Using intersect(…) 、 Using union(…) 和 Using sort_union(…)
图片.png

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = ‘a’ OR key3 = ‘a’;
sql
mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = ‘a’ OR key3 = ‘a’;

图片.png
图片.png

Zero limit
图片.png
mysql> EXPLAIN SELECT * FROM s1 LIMIT 0;
sql
mysql> EXPLAIN SELECT * FROM s1 LIMIT 0;

图片.png

Using filesort
图片.png
mysql> EXPLAIN SELECT * FROM s1 ORDER BY key1 LIMIT 10;
sql
mysql> EXPLAIN SELECT * FROM s1 ORDER BY key1 LIMIT 10;

图片.png
图片.png

mysql> EXPLAIN SELECT * FROM s1 ORDER BY common_field LIMIT 10;
sql
mysql> EXPLAIN SELECT * FROM s1 ORDER BY common_field LIMIT 10;

图片.png

图片.png

Using temporary
mysql> EXPLAIN SELECT DISTINCT common_field FROM s1;
sql
mysql> EXPLAIN SELECT DISTINCT common_field FROM s1;

图片.png

再比如:

mysql> EXPLAIN SELECT common_field, COUNT() AS amount FROM s1 GROUP BY common_field;
sql
mysql> EXPLAIN SELECT common_field, COUNT(
) AS amount FROM s1 GROUP BY common_field;

图片.png

图片.png

mysql> EXPLAIN SELECT key1, COUNT() AS amount FROM s1 GROUP BY key1;
sql
mysql> EXPLAIN SELECT key1, COUNT(
) AS amount FROM s1 GROUP BY key1;

图片.png

从 Extra 的 Using index 的提示里我们可以看出,上述查询只需要扫描 idx_key1 索引就可以搞
定了,不再需要临时表了。

其它
其它特殊情况这里省略。

  1. 小结
    EXPLAIN不考虑各种Cache
    EXPLAIN不能显示MySQL在执行查询时所作的优化工作
    EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况
    部分统计信息是估算的,并非精确值

  2. EXPLAIN的进一步使用
    7.1 EXPLAIN四种输出格式
    这里谈谈EXPLAIN的输出格式。EXPLAIN可以输出四种格式: 传统格式 , JSON格式 , TREE格式 以及 可 视化输出 。用户可以根据需要选择适用于自己的格式。

  3. 传统格式
    传统格式简单明了,输出是一个表格形式,概要说明查询计划。

mysql> EXPLAIN SELECT s1.key1, s2.key1 FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1
WHERE s2.common_field IS NOT NULL;
sql
mysql> EXPLAIN SELECT s1.key1, s2.key1 FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1
WHERE s2.common_field IS NOT NULL;

图片.png

  1. JSON格式
    图片.png

JSON格式:在EXPLAIN单词和真正的查询语句中间加上 FORMAT=JSON 。
EXPLAIN FORMAT=JSON SELECT …
sql
EXPLAIN FORMAT=JSON SELECT …

图片.png

图片.png

图片.png

图片.png
图片.png
图片.png
图片.png
图片.png

我们使用 # 后边跟随注释的形式为大家解释了 EXPLAIN FORMAT=JSON 语句的输出内容,但是大家可能
有疑问 “cost_info” 里边的成本看着怪怪的,它们是怎么计算出来的?先看 s1 表的 “cost_info” 部
分:

“cost_info”: {
“read_cost”: “1840.84”,
“eval_cost”: “193.76”,
“prefix_cost”: “2034.60”,
“data_read_per_join”: “1M”
}
html
“cost_info”: {
“read_cost”: “1840.84”,
“eval_cost”: “193.76”,
“prefix_cost”: “2034.60”,
“data_read_per_join”: “1M”
}

read_cost 是由下边这两部分组成的:
IO 成本
检测 rows × (1 - filter) 条记录的 CPU 成本
小贴士: rows和filter都是我们前边介绍执行计划的输出列,在JSON格式的执行计划中,rows
相当于rows_examined_per_scan,filtered名称不变。

eval_cost 是这样计算的:

检测 rows × filter 条记录的成本。

prefix_cost 就是单独查询 s1 表的成本,也就是:

read_cost + eval_cost

data_read_per_join 表示在此次查询中需要读取的数据量。

对于 s2 表的 “cost_info” 部分是这样的:

“cost_info”: {
“read_cost”: “968.80”,
“eval_cost”: “193.76”,
“prefix_cost”: “3197.16”,
“data_read_per_join”: “1M”
}
html
“cost_info”: {
“read_cost”: “968.80”,
“eval_cost”: “193.76”,
“prefix_cost”: “3197.16”,
“data_read_per_join”: “1M”
}

由于 s2 表是被驱动表,所以可能被读取多次,这里的 read_cost 和 eval_cost 是访问多次 s2 表后累
加起来的值,大家主要关注里边儿的 prefix_cost 的值代表的是整个连接查询预计的成本,也就是单
次查询 s1 表和多次查询 s2 表后的成本的和,也就是:

968.80 + 193.76 + 2034.60 = 3197.16
html
968.80 + 193.76 + 2034.60 = 3197.16

  1. TREE格式
    TREE格式是8.0.16版本之后引入的新格式,主要根据查询的 各个部分之间的关系 和 各部分的执行顺序 来描述如何查询。

mysql> EXPLAIN FORMAT=tree SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key2 WHERE
s1.common_field = ‘a’\G
*************************** 1. row ***************************
EXPLAIN: -> Nested loop inner join (cost=1360.08 rows=990)
-> Filter: ((s1.common_field = ‘a’) and (s1.key1 is not null)) (cost=1013.75 rows=990)
-> Table scan on s1 (cost=1013.75 rows=9895)
-> Single-row index lookup on s2 using idx_key2 (key2=s1.key1), with index
condition: (cast(s1.key1 as double) = cast(s2.key2 as double)) (cost=0.25 rows=1)
1 row in set, 1 warning (0.00 sec)
sql
mysql> EXPLAIN FORMAT=tree SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key2 WHERE
s1.common_field = ‘a’\G
*************************** 1. row ***************************
EXPLAIN: -> Nested loop inner join (cost=1360.08 rows=990)
-> Filter: ((s1.common_field = ‘a’) and (s1.key1 is not null)) (cost=1013.75 rows=990)
-> Table scan on s1 (cost=1013.75 rows=9895)
-> Single-row index lookup on s2 using idx_key2 (key2=s1.key1), with index
condition: (cast(s1.key1 as double) = cast(s2.key2 as double)) (cost=0.25 rows=1)
1 row in set, 1 warning (0.00 sec)

  1. 可视化输出
    可视化输出,可以通过MySQL Workbench可视化查看MySQL的执行计划。通过点击Workbench的放大镜图
    标,即可生成可视化的查询计划。

图片.png

上图按从左到右的连接顺序显示表。红色框表示 全表扫描 ,而绿色框表示使用 索引查找 。对于每个表,
显示使用的索引。还要注意的是,每个表格的框上方是每个表访问所发现的行数的估计值以及访问该表
的成本。

7.2 SHOW WARNINGS的使用
在我们使用EXPLAIN语句查看了某个查询的执行计划后,紧接着还可以使用SHOW WARNINGS语句查看与这个查询的执行计划有关的一些扩展信息,比如这样:

mysql> EXPLAIN SELECT s1.key1, s2.key1 FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1
WHERE s2.common_field IS NOT NULL;
sql
mysql> EXPLAIN SELECT s1.key1, s2.key1 FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1
WHERE s2.common_field IS NOT NULL;

图片.png

mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
Level: Note
Code: 1003
Message: /* select#1 / select atguigu.s1.key1 AS key1,atguigu.s2.key1
AS key1 from atguigu.s1 join atguigu.s2 where ((atguigu.s1.key1 =
atguigu.s2.key1) and (atguigu.s2.common_field is not null))
1 row in set (0.00 sec)
sql
mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
Level: Note
Code: 1003
Message: /
select#1 */ select atguigu.s1.key1 AS key1,atguigu.s2.key1
AS key1 from atguigu.s1 join atguigu.s2 where ((atguigu.s1.key1 =
atguigu.s2.key1) and (atguigu.s2.common_field is not null))
1 row in set (0.00 sec)

图片.png

  1. 分析优化器执行计划:trace
    图片.png

SET optimizer_trace=“enabled=on”,end_markers_in_json=on;
set optimizer_trace_max_mem_size=1000000;
sql
SET optimizer_trace=“enabled=on”,end_markers_in_json=on;
set optimizer_trace_max_mem_size=1000000;

开启后,可分析如下语句:

SELECT
INSERT
REPLACE
UPDATE
DELETE
EXPLAIN
SET
DECLARE
CASE
IFRETURN
CALL
测试:执行如下SQL语句

select * from student where id < 10;
sql
select * from student where id < 10;

最后, 查询 information_schema.optimizer_trace 就可以知道MySQL是如何执行SQL的 :

select * from information_schema.optimizer_trace\G
sql
select * from information_schema.optimizer_trace\G

图片.png

图片.png
图片.png
图片.png

MySQL监控分析视图-sys schema

图片.png

9.1 Sys schema视图摘要
主机相关:以host_summary开头,主要汇总了IO延迟的信息。
Innodb相关:以innodb开头,汇总了innodb buffer信息和事务等待innodb锁的信息。
I/o相关:以io开头,汇总了等待I/O、I/O使用量情况。
内存使用情况:以memory开头,从主机、线程、事件等角度展示内存的使用情况
连接与会话信息:processlist和session相关视图,总结了会话相关信息。
表相关:以schema_table开头的视图,展示了表的统计信息。
索引信息:统计了索引的使用情况,包含冗余索引和未使用的索引情况。
语句相关:以statement开头,包含执行全表扫描、使用临时表、排序等的语句信息。
用户相关:以user开头的视图,统计了用户使用的文件I/O、执行语句统计信息。
等待事件相关信息:以wait开头,展示等待事件的延迟情况。
9.2 Sys schema视图使用场景
索引情况

#1. 查询冗余索引 
select * from sys.schema_redundant_indexes; 
#2. 查询未使用过的索引 select * from sys.schema_unused_indexes; 
#3. 查询索引的使用情况 select index_name,rows_selected,rows_inserted,rows_updated,rows_deleted
from sys.schema_index_statistics where table_schema='dbname' ;
sql
#1. 查询冗余索引 
select * from sys.schema_redundant_indexes; 
#2. 查询未使用过的索引 select * from sys.schema_unused_indexes; 
#3. 查询索引的使用情况 select index_name,rows_selected,rows_inserted,rows_updated,rows_deleted
from sys.schema_index_statistics where table_schema='dbname' ;

表相关

# 1. 查询表的访问量 
select table_schema,table_name,sum(io_read_requests+io_write_requests) as io from
 sys.schema_table_statistics group by table_schema,table_name order by io desc; 

# 2. 查询占用bufferpool较多的表 
select object_schema,object_name,allocated,data
from sys.innodb_buffer_stats_by_table order by allocated limit 10; 

# 3. 查看表的全表扫描情况 
select * from sys.statements_with_full_table_scans where db='dbname';
sql
# 1. 查询表的访问量 
select table_schema,table_name,sum(io_read_requests+io_write_requests) as io from
 sys.schema_table_statistics group by table_schema,table_name order by io desc; 

# 2. 查询占用bufferpool较多的表 
select object_schema,object_name,allocated,data
from sys.innodb_buffer_stats_by_table order by allocated limit 10; 

# 3. 查看表的全表扫描情况 
select * from sys.statements_with_full_table_scans where db='dbname';

语句相关

#1. 监控SQL执行的频率 
select db,exec_count,query from sys.statement_analysis order by exec_count desc; 

#2. 监控使用了排序的SQL 
select db,exec_count,first_seen,last_seen,query
from sys.statements_with_sorting limit 1; 

#3. 监控使用了临时表或者磁盘临时表的SQL 
select db,exec_count,tmp_tables,tmp_disk_tables,query
from sys.statement_analysis where tmp_tables>0 or tmp_disk_tables >0 order by
 (tmp_tables+tmp_disk_tables) desc;
sql
#1. 监控SQL执行的频率 
select db,exec_count,query from sys.statement_analysis order by exec_count desc; 

#2. 监控使用了排序的SQL 
select db,exec_count,first_seen,last_seen,query
from sys.statements_with_sorting limit 1; 

#3. 监控使用了临时表或者磁盘临时表的SQL 
select db,exec_count,tmp_tables,tmp_disk_tables,query
from sys.statement_analysis where tmp_tables>0 or tmp_disk_tables >0 order by
 (tmp_tables+tmp_disk_tables) desc;

IO相关

#1. 查看消耗磁盘IO的文件 
select file,avg_read,avg_write,avg_read+avg_write as avg_io
from sys.io_global_by_file_by_bytes order by avg_read limit 10;
sql
#1. 查看消耗磁盘IO的文件 
select file,avg_read,avg_write,avg_read+avg_write as avg_io
from sys.io_global_by_file_by_bytes order by avg_read limit 10;

Innodb 相关

#1. 行锁阻塞情况 
select * from sys.innodb_lock_waits;
sql
#1. 行锁阻塞情况 
select * from sys.innodb_lock_waits;

图片.png

10 小结
查询是数据库中最频繁的操作,提高查询速度可以有效地提高MysQL数据库的性能。通过对查询语句的分析可以了解查询语句的执行情况,找出查语句的瓶颈,从而优化查询语句。

都有哪些维度可以进行数据库调优?简言之:

索引失效、没有充分利用到索引――索引建立

关联查询太多JOIN(设计缺陷或不得已的需求)——SQL优化

服务器调优及各个参数设置(缓冲、线程数等)――调整my.cnf

数据过多一分库分表

关于数据库调优的知识点非常分散。不同的DBMS,不同的公司,不同的职位,不同的项目遇到的问题都不尽相同。这里我们分为三个章节进行细致讲解。

虽然SQL查询优化的技术有很多,但是大方向上完全可以分成物理查询优化和逻辑查询优化两大块。

物理查询优化是通过索引和表连接方式等技术来进行优化,这里重点需要掌握索引的使用。

逻辑查询优化就是通过SQL等价变换提升查询效率,直白一点就是说,换一种查询写法执行效率可能更高。

索引优化与查询优化

数据准备

学员表 插 50万 条, 班级表 插 1万 条。

步骤1:建表

CREATE TABLE `class` ( 
	`id` INT(11) NOT NULL AUTO_INCREMENT, 
	`className` VARCHAR(30) DEFAULT NULL, 
	`address` VARCHAR(40) DEFAULT NULL, 
	`monitor` INT NULL , PRIMARY KEY (`id`) 
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 

CREATE TABLE `student` ( 
	`id` INT(11) NOT NULL AUTO_INCREMENT, 
	`stuno` INT NOT NULL , 
	`name` VARCHAR(20) DEFAULT NULL, 
	`age` INT(3) DEFAULT NULL, 
	`classId` INT(11) DEFAULT NULL, 
	PRIMARY KEY (`id`) 
#CONSTRAINT `fk_class_id` FOREIGN KEY (`classId`) REFERENCES `t_class` (`id`) 
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

步骤2:设置参数
命令开启:允许创建函数设置:

set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。

步骤3:创建函数
保证每条数据都不同。

#随机产生字符串 
DELIMITER // 
CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255) 
BEGIN 
DECLARE chars_str VARCHAR(100) DEFAULT
 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ'; 
DECLARE return_str VARCHAR(255) DEFAULT ''; 
DECLARE i INT DEFAULT 0; 
WHILE i < n DO 
SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1)); 
SET i = i + 1; 
END WHILE;
RETURN return_str; 
END // 
DELIMITER ; 

#假如要删除 
#drop function rand_string;

随机产生班级编号

#用于随机产生多少到多少的编号 
DELIMITER // 
CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11) 
BEGIN 
DECLARE i INT DEFAULT 0; SET i = FLOOR(from_num +RAND()*(to_num - from_num+1)) ; 
RETURN i; 
END // 
DELIMITER ; 

#假如要删除 
#drop function rand_num;

步骤4:创建存储过程

#创建往stu表中插入数据的存储过程 
DELIMITER // 
CREATE PROCEDURE insert_stu( START INT , max_num INT ) 
BEGIN 
DECLARE i INT DEFAULT 0; 
SET autocommit = 0; #设置手动提交事务 
REPEAT #循环 
SET i = i + 1; #赋值 
INSERT INTO student (stuno, name ,age ,classId ) VALUES ((START+i),rand_string(6),rand_num(1,50),rand_num(1,1000)); 
UNTIL i = max_num END REPEAT; 
COMMIT; #提交事务 
END // 
DELIMITER ; 

#假如要删除 
#drop PROCEDURE insert_stu;

创建往class表中插入数据的存储过程

#执行存储过程,往class表添加随机数据 
DELIMITER // 
CREATE PROCEDURE `insert_class`( max_num INT ) BEGIN DECLARE i INT DEFAULT 0; 
SET autocommit = 0; 
REPEAT 
SET i = i + 1; 
INSERT INTO class ( classname,address,monitor ) VALUES
 (rand_string(8),rand_string(10),rand_num(1,100000)); 
UNTIL i = max_num 
END REPEAT; 
COMMIT;
END // 
DELIMITER ; 

#假如要删除 
#drop PROCEDURE insert_class;

步骤5:调用存储过程
class

#执行存储过程,往class表添加1万条数据 
CALL insert_class(10000);

stu

#执行存储过程,往stu表添加50万条数据 
CALL insert_stu(100000,500000);

步骤6:删除某表上的索引
创建存储过程

DELIMITER // 
CREATE PROCEDURE `proc_drop_index`(dbname VARCHAR(200),tablename VARCHAR(200)) 
BEGIN 
DECLARE done INT DEFAULT 0; 
DECLARE ct INT DEFAULT 0; 
DECLARE _index VARCHAR(200) DEFAULT ''; 
DECLARE _cur CURSOR FOR SELECT index_name FROM 
information_schema.STATISTICS WHERE table_schema=dbname AND 
table_name=tablename AND seq_in_index=1 AND index_name <>'PRIMARY' ; 
#每个游标必须使用不同的declare continue handler for not found set done=1来控制游标的结束 
DECLARE CONTINUE HANDLER FOR NOT FOUND set done=2 ; 
#若没有数据返回,程序继续,并将变量done设为2 
OPEN _cur; 
FETCH _cur INTO _index; 
WHILE _index<>'' DO 
SET @str = CONCAT("drop index " , _index , " on " , tablename ); 
PREPARE sql_str FROM @str ; 
EXECUTE sql_str; 
DEALLOCATE PREPARE sql_str; 
SET _index=''; 
FETCH _cur INTO _index; 
END WHILE; 
CLOSE _cur; 
END // 
DELIMITER ;

执行存储过程

CALL proc_drop_index("dbname","tablename");

索引失效案例

MySQL中提高性能的一个最有效的方式是对数据表设计合理的索引。索引提供了高效访问数据的方法,并且加快查询的速度,因此索引对查询的速度有着至关重要的影响。

使用索引可以快速地定位表中的某条记录,从而提高数据库查询的速度,提高数据库的性能。

如果查询时没有使用索引,查询语句就会扫描表中的所有记录。在数据量大的情况下,这样查询的速度会很慢。

大多数情况下都(默认)采用B+树来构建索引。只是空间列类型的索引使用R-树,并且MEMORY表还支持hash索引。

其实,用不用索引,最终都是优化器说了算。优化器是基于什么的优化器?基于cost开销
(CostBaseOptimizer),它不是基于规则(Rule-BasedOptimizer),也不是基于语义。怎么样开销小就怎么来。另外,SQL语句是否使用索引,跟数据库版本、数据量、数据选择度都有关系。

2.1 全值匹配我最爱
系统中经常出现的sql语句如下:

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 and classId=4;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 and classId=4 AND name = ‘abcd’ ;
sql
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 and classId=4;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 and classId=4 AND name = ‘abcd’ ;

建立索引前执行:(关注执行时间)

mysql> SELECT SQL_NO_CACHE * FROM student WHERE age=30 and classId=4 AND name = 'abcd ';
Empty set,1 warning (0.28 sec)
sql
mysql> SELECT SQL_NO_CACHE * FROM student WHERE age=30 and classId=4 AND name = 'abcd ';
Empty set,1 warning (0.28 sec)

建立索引

CREATE INDEX idx_age oN student( age ) ;
CREATE INDEX idx_age_classid ON student(age , classId );
CREATE INDEX idx_age_classid_name ON student( age ,classId , name ) ;
sql
CREATE INDEX idx_age oN student( age ) ;
CREATE INDEX idx_age_classid ON student(age , classId );
CREATE INDEX idx_age_classid_name ON student( age ,classId , name ) ;

建立索引后执行:

mysql> SELECT SQL_NO_CACHE * FROM student WHERE age=30 and classId=4 AND name = 'abcd ';
Empty set,1 warning (0.01 sec)
sql
mysql> SELECT SQL_NO_CACHE * FROM student WHERE age=30 and classId=4 AND name = 'abcd ';
Empty set,1 warning (0.01 sec)

可以看到,创建索引前的查询时间是0.28秒,创建索引后的查询时间是0.01秒,索引帮助我们极大的提高了查询效率。

2.2 最佳左前缀法则
在MySQL建立联合索引时会遵守最佳左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。

举例1:

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.name = ‘abcd’ ;
sql
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.name = ‘abcd’ ;

举例2:

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.classid=1 AND student . name = ‘abcd’;
sql
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.classid=1 AND student . name = ‘abcd’;

举例3:索引idx_age_classid_name还能否正常使用?

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE classid=4 AND student.age=30 AND student.name= ‘abcd’
sql
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE classid=4 AND student.age=30 AND student.name= ‘abcd’

如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。

mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student. name =’ abcd ’ ;
sql
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student. name =’ abcd ’ ;

image.png

虽然可以正常使用,但是只有部分被使用到了。

mysq1>EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.classid=1 AND student.name ='abcd ’ ;
sql
mysq1>EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.classid=1 AND student.name ='abcd ’ ;

image.png
完全没有使用上索引。

结论:MySQL可以为多个字段创建索引,一个索引可以包括16个字段。对于多列索引,过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。如果查询条件中没有使用这些字段中第1个字段时,多列(或联合)索引不会被使用。

拓展:Alibaba《Java开发手册》
索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。

2.3 主键插入顺序
对于一个使用InnoDB存储引擎的表来说,在我们没有显式的创建索引时,表中的数据实际上都是存储在聚簇索引的叶子节点的。而记录又是存储在数据页中的,数据页和话录又是按照记录主键值从小到大的顺序进行排序,所以如果我们插入的记录的主键值是依次增大的话,那我们每插满一个数据页就换到下一个数据页继续插,而如果我们插入的主键值忽大忽小的话,就比较麻烦了,假设某个数据页存储的记录已经满了,它存储的主键值在1~100之间:

image.png

如果此时再插入一条主键值为 9 的记录,那它插入的位置就如下图:

image.png

可这个数据页已经满了,再插进来咋办呢?我们需要把当前 页面分裂 成两个页面,把本页中的一些记录
移动到新创建的这个页中。页面分裂和记录移位意味着什么?意味着: 性能损耗 !所以如果我们想尽量
避免这样无谓的性能损耗,最好让插入的记录的 主键值依次递增 ,这样就不会发生这样的性能损耗了。
所以我们建议:让主键具有 AUTO_INCREMENT ,让存储引擎自己为表生成主键,而不是我们手动插入 ,
比如: person_info 表:

CREATE TABLE person_info(
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
birthday DATE NOT NULL,
phone_number CHAR(11) NOT NULL,
country varchar(100) NOT NULL,
PRIMARY KEY (id),
KEY idx_name_birthday_phone_number (name(10),
birthday, phone_number)
);

我们自定义的主键列 id 拥有 AUTO_INCREMENT 属性,在插入记录时存储引擎会自动为我们填入自增的
主键值。这样的主键占用空间小,顺序写入,减少页分裂。

2.4 计算、函数、类型转换(自动或手动)导致索引失效
这两条sql哪种写法更好
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE ‘abc%’;

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = ‘abc’;

创建索引
CREATE INDEX idx_name ON student(NAME);

第一种:索引优化生效
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE ‘abc%’;

mysql> SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE ‘abc%’;

第二种:索引优化失效

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = ‘abc’;

SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = ‘abc’;

type为“ALL”,表示没有使用到索引,查询时间为 3.62 秒,查询效率较之前低很多。

再举例:

student表的字段stuno上设置有索引
CREATE INDEX idx_sno ON student(stuno);
sql
CREATE INDEX idx_sno ON student(stuno);

索引优化失效:(假设: student表的字段stuno上设置有索引
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno+1 = 900001;
sql
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno+1 = 900001;

运行结果:

image.png

你能看到如果对索引进行了表达式计算,索引就失效了。这是因为我们需要把索引字段的取值都取出来,然后依次进行表达式的计算来进行条件判断,因此采用的就是全表扫描的方式,运行时间也会慢很多,最终运行时间为2.538秒。

索引优化生效:
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno = 900000;
sql
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno = 900000;

运行时间为0.039秒。

再举例:

student表的字段name上设置有索引
CREATE INDEX idx_name ON student(NAME);
sql
CREATE INDEX idx_name ON student(NAME);

我们想要对name的前三位为abc的内容进行条件筛选,这里我们来查看下执行计划:

索引优化失效:
EXPLAIN SELECT id, stuno, name FROM student WHERE SUBSTRING(name, 1,3)=‘abc’;
sql
EXPLAIN SELECT id, stuno, name FROM student WHERE SUBSTRING(name, 1,3)=‘abc’;

image.png

索引优化生效:
EXPLAIN SELECT id, stuno, NAME FROM student WHERE NAME LIKE ‘abc%’;
sql
EXPLAIN SELECT id, stuno, NAME FROM student WHERE NAME LIKE ‘abc%’;

image.png

你能看到经过查询重写后,可以使用索引进行范围检索,从而提升查询效率。

2.5 类型转换导致索引失效
下列哪个sql语句可以用到索引。(假设name字段上设置有索引)

# 未使用到索引 
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name=123;

# 使用到索引 
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name='123';

name=123发生类型转换,索引失效。
结论:设计实体类属性时,一定要与数据库字段类型相对应。否则,就会出现类型转换的情况。

2.6 范围条件右边的列索引失效
如果系统经常出现的sql如下:
ALTER TABLE student DROP INDEX idx_name;
ALTER TABLE student DROP INDEX idx_age;
ALTER TABLE student DROP INDEX idx_age_classid;

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.classId>20 AND
student.name = ‘abc’ ;

那么索引 idx_age_classid_name这个索引还能正常使用么?
不能,范围右边的列不能使用。比如: (<) (<=) (>)(>=)和between等。如果这种sql出现较多,应该建立:
create index idx_age_name_classid on student(age,name,classid);

将范围查询条件放置语句最后:
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.name = ‘abc’ AND
student.classId>20 ;

应用开发中范围查询,例如:金额查询,日期查询往往都是范围查询。应将查询条件放置where语句最后。

效果
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.classId>20 ANDstudent.name = 'abc ’ ;

image.png

2.7 不等于(!= 或者<>)索引失效
为name字段创建索引
CREATE INDEX idx_name oN student(NAME);

查看索引是否失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name <> ‘abc’ ;

或者

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student. name != ‘abc’;

image.png

场景举例:用户提出需求,将财务数据,产品利润金额不等于0的都统计出来。

2.8 is null可以使用索引,is not null无法使用索引
IS NULL:可以触发索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NULL;

lS NOT NULL:无法触发索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NOT NULL;

结论:最好在设计数据表的时候就将字段设置为NOT NULL 约束,比如你可以将INT类型的字段,默认值设置为0。将字符类型的默认值设置为空字符串(‘’)。
拓展:同理,在查询中使用not like 也无法使用索引,导致全表扫描。

2.9 like以通配符%开头索引失效
在使用LIKE关键字进行查询的查询语句中,如果匹配字符串的第一个字符为“%”,索引就不会起作用。只有“%"不在第一个位置,索引才会起作用。

使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name LIKE 'ab% ';

未使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name LIKE '%ab% ;

拓展:Alibaba《Java开发手册》
【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。

2.10 OR 前后存在非索引的列,索引失效
在WHERE子句中,如果在OR前的条件列进行了索引,而在OR后的条件列没有进行索引,那么索引会失效。也就是说,OR前后的两个条件中的列都是索引时,查询中才使用索引。

因为OR的含义就是两个只要满足一个即可,因此只有一个条件列进行了索引是没有意义的,只要有条件列没有进行索引,就会进行全表扫描,因此索引的条件列也会失效。

查询语句使用OR关键字的情况:

# 未使用到索引 
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR classid = 100;
sql

因为classid字段上没有索引,所以上述查询语句没有使用索引。

#使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR name = ‘Abel’;

因为age字段和name字段上都有索引,所以查询中使用了索引。你能看到这里使用到了index_merge,简单来说index_merge就是对age和name分别进行了扫描,然后将这两个结果集进行了合并。这样做的好处就是避免了全表扫描。

2.11 数据库和表的字符集统一使用utf8mb4
统一使用utf8mb4( 5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。不同的 字符集 进行比较前需要进行 转换 会造成索引失效。

2.12 练习及一般性建议
练习:假设: index(a,b,c)

一般性建议:

对于单列索引,尽量选择针对当前query过滤性更好的索引
在选择组合索引的时候,当前query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。
在选择组合索引的时候,尽量选择能够包含当前query中的where子句中更多字段的索引。
在选择组合索引的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引次序的最后面。
总之,书写SQL语句时,尽量避免造成索引失效的情况。

关联查询优化

3.1 数据准备

#分类
CREATE TABLE IF NOT EXISTS `type`(
	`id` INT (10) UNSIGNED NOT NULL AUTO_INCREMENT,
	`card` INT(10) UNSIGNED NOT NULL,
	PRIMARY KEY ( `id`)
);

#图书
CREATE TABLE IF NOT EXISTS `book`(
	`bookid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	`card` INT(10) UNSIGNED NOT NULL,
	PRIMARY KEY ( `bookid`)
);

#向分类表中添加20条记录
INSERT INTO type( card) VALUES( FLOOR(1 +(RAND( ) * 20) ) );
INSERT INTO type(card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) ) ;
INSERT INTO type(card) VALUES(FLOOR( 1 +(RAND( ) * 20) ) ) ;
INSERT INTO type(card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) ) ;
INSERT INTO type( card) VALUES( FLOOR(1 +(RAND( ) * 20) ) ) ;
INSERT INTO type (card) VALUES( FLOOR( 1 +(RAND() * 20) ) ) ;
INSERT INTO type( card) VALUES( FLOOR( 1 + (RAND( ) * 20) ) );
INSERT INTO type( card) VALUES( FLOOR(1 +(RAND( ) * 20) ) ) ;
INSERT INTO type ( card) VALUES(FLOOR( 1 +(RAND( ) * 20) ) );
INSERT INTO type( card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) );
INSERT INTO type ( card) VALUES(FLOOR( 1 +(RAND( ) * 20) ) ) ;
INSERT INTO type(card) VALUES(FLOOR(1 +(RAND() * 20) ) );
INSERT INTO type( card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) );
INSERT INTO type(card) VALUES( FLOOR(1 + (RAND() * 20) ) ) ;
INSERT INTO type( card) VALUES( FLOOR(1 + (RAND() * 20) ) ) ;
INSERT INTO type(card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) );
INSERT INTO type( card ) VALUES( FLOOR(1 +(RAND() * 20) ) );
INSERT INTO type( card ) VALUES( FLOOR(1 +(RAND() * 20) ) );
INSERT INTO type( card ) VALUES( FLOOR(1 +(RAND() * 20) ) );
INSERT INTO type( card ) VALUES( FLOOR(1 +(RAND() * 20) ) );


#向图书表中添加20条记录
INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND() * 20) ) );
INSERT INTO book(card) VALUES(FLOOR(1 +(RAND() * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR( 1 + (RAND() * 20) ) );
INSERT INTO book(card) VALUES(FLOOR( 1 +(RAND() * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR( 1 + (RAND( ) * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) );
INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) );
INSERT INTO book(card) VALUES(FLOOR(1 +(RAND() * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR(1 +(RAND( ) * 20) ) );
INSERT INTO book(card) VALUES( FLOOR(1 +(RAND() * 20) ) ) ;
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR(1 +(RAND() * 20) ) );
INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND() * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND() * 20) ) );
INSERT INTO book(card) VALUES(FLOOR(1 +(RAND( ) * 20) ) );
INSERT INTO book(card) VALUES( FLOOR( 1 + (RAND( ) * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) );
INSERT INTO book(card) VALUES( FLOOR(1 +(RAND( ) * 20) ) );
INSERT INTO book(card) VALUES( FLOOR(1 +(RAND( ) * 20) ) ) ;

3.2 采用左外连接
下面开始 EXPLAIN 分析

EXPLAIN SELECT SQL_NO_CACHE * FROM type LEFT JOIN book ON type.card = book.card;

结论:type 有All

添加索引优化

ALTER TABLE book ADD INDEX Y ( card); #【被驱动表】,可以避免全表扫描

EXPLAIN SELECT SQL_NO_CACHE * FROM type LEFT JOIN book ON type.card = book.card;

可以看到第二行的 type 变为了 ref,rows 也变成了优化比较明显。这是由左连接特性决定的。LEFT JOIN条件用于确定如何从右表搜索行,左边一定都有,所以 右边是我们的关键点,一定需要建立索引 。

ALTER TABLE type ADD INDEX X (card); #【驱动表】,无法避免全表扫描

EXPLAIN SELECT SQL_NO_CACHE * FROM type LEFT JOIN book ON type.card = book.card;

接着:

DROP INDEX Y ON book;
EXPLAIN SELECT SQL_NO_CACHE * FROM type LEFT JOIN book ON type.card = book.card;

3.3 采用内连接
drop index X on type;
drop index Y on book;(如果已经删除了可以不用再执行该操作)

换成 inner join(MySQL自动选择驱动表)

EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;

添加索引优化

ALTER TABLE book ADD INDEX Y ( card);

EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;

ALTER TABLE type ADD INDEX X (card);

EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;

接着:

DROP INDEX X ON type;

EXPLAIN SELECT SQL_NO_CACHE * FROM TYPE INNER JOIN book ON type.card=book.card;

接着:

ALTER TABLE type ADD INDEX X (card);

EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;

接着:

#向表中再添加2日条记录
INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND() * 20) ) );
INSERT INTO book(card) VALUES(FLOOR(1 +(RAND() * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR( 1 + (RAND() * 20) ) );
INSERT INTO book(card) VALUES(FLOOR( 1 +(RAND() * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR( 1 + (RAND( ) * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) );
INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) );
INSERT INTO book(card) VALUES(FLOOR(1 +(RAND() * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR(1 +(RAND( ) * 20) ) );
INSERT INTO book(card) VALUES( FLOOR(1 +(RAND() * 20) ) ) ;
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR(1 +(RAND() * 20) ) );
INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND() * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND() * 20) ) );
INSERT INTO book(card) VALUES(FLOOR(1 +(RAND( ) * 20) ) );
INSERT INTO book(card) VALUES( FLOOR( 1 + (RAND( ) * 20) ) ) ;
INSERT INTO book(card) VALUES( FLOOR( 1 +(RAND( ) * 20) ) );
INSERT INTO book(card) VALUES( FLOOR(1 +(RAND( ) * 20) ) );
INSERT INTO book(card) VALUES( FLOOR(1 +(RAND( ) * 20) ) ) ;

ALTER TABLE book ADD INDEX Y ( card) ;

EXPLAIN SELECT SQL_NO_CACHE * FROM TYPE INNER JOIN book ON type.card=book.card;

图中发现,由于type表数据大于book表数据,MySQL选择将type作为被驱动表。也就是小表驱动大表。

3.4 join语句原理
join方式连接多个表,本质就是各个表之间数据的循环匹配。MySQL5.5版本之前,MySQL只支持一种表间关联方式,就是嵌套循环(Nested Loop Join)。如果关联表的数据量很大,则join关联的执行时间会非常长。在MySQL5.5以后的版本中,MySQL通过引入BNLJ算法来优化嵌套执行。

1.驱动表和被驱动表
驱动表就是主表,被驱动表就是从表、非驱动表。

对于内连接来说:
SELECT * FROM A JOIN B ON …

A一定是驱动表吗?不一定,优化器会根据你查询语句做优化,决定先查哪张表。先查询的那张表就是驱动表,反之就是被驱动表。通过explain关键字可以查看。

对于外连接来说:
SELECT * FROM A LEFT JOIN B ON …

#或

SELECT * FROM B RIGHT JOIN A ON …

#或

SELECT * FROM B RIGHT JOIN A ON …

通常,大家会认为A就是驱动表,B就是被驱动表。但也未必。测试如下:

CREATE TABLE a(f1 INT, f2 INT,INDEX(f1 ))ENGINE=INNODB;
CREATE TABLE b(f1 INT,f2 INT)ENGINE=INNODB;

INSERT INTO a VALUES( 1,1),(2,2),(3,3) ,(4,4),(5,5),(6,6);
INSERT INTO b VALUES(3,3 ), (4,4),(5,5),(6,6),(7,7),(8,8);

SELECT * FROM b;

#测试1
EXPLAIN SELECT * FROM a LEFT JOIN b ON( a.f1=b.f1) WHERE (a.f2=b.f2);
#测试2
EXPLAIN SELECT * FROM a LEFT JOIN b ON(a.f1=b.f1) AND ( a.f2=b.f2);

  1. Simple Nested-Loop Join(简单嵌套循环连接)
    算法相当简单,从表A中取出一条数据1,遍历表B,将匹配到的数据放到result…以此类推,驱动表A中的每一条记录与被驱动表B的记录进行判断:

image.png

可以看到这种方式效率是非常低的,以上述表A数据100条,表B数据1000条计算,则A*B=10万次。开销统计如下:

image.png

当然mysql肯定不会这么粗暴的去进行表的连接,所以就出现了后面的两种对Nested-Loop Join优化算法。

  1. Index Nested-Loop Join(索引嵌套循环连接)
    Index Nested-Loop Join其优化的思路主要是为了减少内层表数据的匹配次数,所以要求被驱动表上必须有索引才行。通过外层表匹配条件直接与内层表索引进行匹配,避免和内层表的每条记录去进行比较,这样极大的减少了对内层表的匹配次数。

image.png

驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本是比较固定的,故mysql优化器都倾向于使用记录数少的表作为驱动表(外表)。

image.png

如果被驱动表加索引,效率是非常高的,但如果索引不是主键索引,所以还得进行一次回表查询。相比,被驱动表的索引是主键索引,效率会更高。

  1. Block Nested-Loop Join(块嵌套循环连接)
    如果存在索引,那么会使用index的方式进行join,如果join的列没有索引,被驱动表要扫描的次数太多了。每次访问被驱动表,其表中的记录都会被加载到内存中,然后再从驱动表中取一条与其匹配,匹配结束后清除内存,然后再从驱动表中加载一条记录,然后把被驱动表的记录在加载到内存匹配,这样周而复始,大大增加了IO的次数。为了减少被驱动表的IO次数,就出现了Block Nested-Loop Join的方式。

不再是逐条获取驱动表的数据,而是一块一块的获取,引入了join buffer缓冲区,将驱动表join相关的部分数据列(大小受join buffer的限制)缓存到join buffer中,然后全表扫描被驱动表,被驱动表的每一条记录一次性和joinbuffer中的所有驱动表记录进行匹配(内存中操作),将简单嵌套循环中的多次比较合并成一次,降低了被驱动表的访问频率。

注意:
这里缓存的不只是关联表的列,select后面的列也会缓存起来。
在一个有N个join关联的sql中会分配N-1个join buffer。所以查询的时候尽量减少不必要的字段,可以让joinbuffer中可以存放更多的列。

image.png

image.png

参数设置:

block_nested_loop
通过 SHOW VARIABLES LIKE '%optimizer_switch%'查看block_nested_loop状态。默认是开启的。

join_buffer_size
驱动表能不能一次加载完,要看join buffer能不能存储所有的数据,默认情况下join_buffer_size=256k 。

mysql> show variables like ‘%join_buffer%’;
±-----------------±-------+
| Variable_name | Value |
±-----------------±-------+
| join_buffer_size | 262144 |
±-----------------±-------+
1 row in set (1.56 sec)

join_buffer_size的最大值在32位系统可以申请4G,而在64位操做系统下可以申请大于4G的Join Buffer空间(64位Windows除外,其大值会被截断为4GB并发出警告)。

  1. Join小结
    1、整体效率比较:INLJ >BNLJ > SNLJ

2、永远用小结果集驱动大结果集(其本质就是减少外层循环的数据数量) (小的度量单位指的是表行数*每行大小)

select t1.b,t2.* from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=100;#推荐
select t1.b,t2.* from t2 straight.join t1 on (t1.b=t2.b) where t2.id<=100;#不推荐
sql
select t1.b,t2.* from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=100;#推荐
select t1.b,t2.* from t2 straight.join t1 on (t1.b=t2.b) where t2.id<=100;#不推荐

3、为被驱动表匹配的条件增加索引(减少内层表的循环匹配次数)

4、增大join buffer size的大小(一次缓存的数据越多,那么内层包的扫表次数就越少)

5、减少驱动表不必要的字段查询(字段越少,join buffer所缓存的数据就越多)

  1. Hash Join
    从MySQL的8.0.20版本开始将废弃BNLJ,因为从MySQL8.0.18版本开始就加入了hash join默认都会使用hash join

Nested Loop:
对于被连接的数据子集较小的情况,Nested Loop是个较好的选择。

Hash Join是做大数据集连接时的常用方式,优化器使用两个表中较小(相对较小)的表利用Join Key在内存中建立散列表,然后扫描较大的表并探测散列表,找出与Hash表匹配的行。

这种方式适用于较小的表完全可以放于内存中的情况,这样总成本就是访问两个表的成本之和。
在表很大的情况下并不能完全放入内存,这时优化器会将它分割成若干不同的分区,不能放入内存的部分就把该分区写入磁盘的临时段,此时要求有较大的临时段从而尽量提高I/o 的性能。
它能够很好的工作于没有索引的大表和并行查询的环境中,并提供最好的性能。大多数人都说它是Join的重型升降机。Hash Join只能应用于等值连接(如WHERE A.COL1=B.COL2),这是由Hash的特点决定的。
image.png

我们来看一下这个语句:

EXPLAIN SELECT * FROM t1 STRAIGHT_JOIN t2 ON (t1.a=t2.a);

如果直接使用join语句,MySQL优化器可能会选择表t1或t2作为驱动表,这样会影响我们分析SQL语句的
执行过程。所以,为了便于分析执行过程中的性能问题,我改用 straight_join 让MySQL使用固定的
连接方式执行查询,这样优化器只会按照我们指定的方式去join。在这个语句里,t1 是驱动表,t2是被驱
动表。

image.png

可以看到,在这条语句里,被驱动表t2的字段a上有索引,join过程用上了这个索引,因此这个语句的执
行流程是这样的:

从表t1中读入一行数据 R;
从数据行R中,取出a字段到表t2里去查找;
取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分;
重复执行步骤1到3,直到表t1的末尾循环结束。
这个过程是先遍历表t1,然后根据从表t1中取出的每行数据中的a值,去表t2中查找满足条件的记录。在
形式上,这个过程就跟我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引,所以我们称之为
“Index Nested-Loop Join”,简称NLJ。

它对应的流程图如下所示:

image.png

在这个流程里:

对驱动表t1做了全表扫描,这个过程需要扫描100行;
而对于每一行R,根据a字段去表t2查找,走的是树搜索过程。由于我们构造的数据都是一一对应的,因此每次的搜索过程都只扫描一行,也是总共扫描100行;
所以,整个执行流程,总扫描行数是200。
引申问题1:能不能使用join?

引申问题2:怎么选择驱动表?

比如:N扩大1000倍的话,扫描行数就会扩大1000倍;而M扩大1000倍,扫描行数扩大不到10倍。

两个结论:

使用join语句,性能比强行拆成多个单表执行SQL语句的性能要好;
如果使用join语句的话,需要让小表做驱动表。
Simple Nested-Loop Join

Block Nested-Loop Join

这个过程的流程图如下:

image.png

执行流程图也就变成这样:

image.png

总结1:能不能使用xxx join语句?

总结2:如果要使用join,应该选择大表做驱动表还是选择小表做驱动表?

总结3:什么叫作“小表”?

在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各
个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。

3.5 小结
保证被驱动表的JOIN字段已经创建了索引
需要JOIN 的字段,数据类型保持绝对一致。
LEFT JOIN 时,选择小表作为驱动表, 大表作为被驱动表 。减少外层循环的次数。
INNER JOIN 时,MySQL会自动将 小结果集的表选为驱动表 。选择相信MySQL优化策略。
能够直接多表关联的尽量直接关联,不用子查询。(减少查询的趟数)
不建议使用子查询,建议将子查询SQL拆开结合程序多次查询,或使用 JOIN 来代替子查询。
衍生表建不了索引

子查询优化

MySQL从4.1版本开始支持子查询,使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结
果作为另一个SELECT语句的条件。 子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作 。

子查询是 MySQL 的一项重要的功能,可以帮助我们通过一个 SQL 语句实现比较复杂的查询。但是,子
查询的执行效率不高。原因:

① 执行子查询时,MySQL需要为内层查询语句的查询结果 建立一个临时表 ,然后外层查询语句从临时表
中查询记录。查询完毕后,再 撤销这些临时表 。这样会消耗过多的CPU和IO资源,产生大量的慢查询。

② 子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都 不会存在索引 ,所以查询性能会
受到一定的影响。

③ 对于返回结果集比较大的子查询,其对查询性能的影响也就越大。

在MySQL中,可以使用连接(JOIN)查询来替代子查询。连接查询 不需要建立临时表 ,其 速度比子查询 要快 ,如果查询中使用索引的话,性能就会更好。

举例1:查询学生表中是班长的学生信息。

使用子查询

#创建班级表中班长的索引
CREATE INDEX idx_monitor on class(monitor);

EXPLAIN SELECT * FROM student stu1 WHERE stu1.`stuno` IN (
SELECT monitor FROM class c
WHERE monitor IS NOT NULL
);

推荐:使用多表查询

EXPLAIN SELECT stu1.* FROM student stu1 JOIN class c ON stu1.`stuno` = c.`monitor`
WHERE c.`monitor` IS NOT NULL ;

举例2:取所有不为班长的同学

不推荐
EXPLAIN SELECT SQL_NO_CACHE a.* FROM student a
WHERE a.stuno NOT IN (
SELECT monitor FROM class b WHERE monitor IS NOT NULL
);

结论:尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代

排序优化

5.1 排序优化
问题:在 WHERE 条件字段上加索引,但是为什么在 ORDER BY 字段上还要加索引呢?

image.png

优化建议:

SQL 中,可以在 WHERE 子句和 ORDER BY 子句中使用索引,目的是在 WHERE 子句中 避免全表扫 描 ,在 ORDER BY 子句 避免使用 FileSort 排序 。当然,某些情况下全表扫描,或者 FileSort 排序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。

尽量使用 Index 完成 ORDER BY 排序。如果 WHERE 和 ORDER BY 后面是相同的列就使用单索引列;如果不同就使用联合索引。

无法使用 Index 时,需要对 FileSort 方式进行调优。

5.2测试
删除student表和class表中已创建的索引。

结论:ORDER BY子句,尽量使用Index方式排序,避免使用File5ort方式排序

小结:

INDEX a_b_c(a,b,c) order by 能使用索引最左前缀
- ORDER BY a
- ORDER BY a,b
- ORDER BY a,b,c
- ORDER BY a DESC,b DESC,c DESC 如果WHERE使用索引的最左前缀定义为常量,则order by 能使用索引
- WHERE a = const ORDER BY b,c
- WHERE a = const AND b = const ORDER BY c
- WHERE a = const ORDER BY b,c
- WHERE a = const AND b > const ORDER BY b,c 不能使用索引进行排序
- ORDER BY a ASC,b DESC,c DESC /* 排序不一致 */
- WHERE g = const ORDER BY b,c /*丢失a索引*/
- WHERE a = const ORDER BY c /*丢失b索引*/
- WHERE a = const ORDER BY a,d /*d不是索引的一部分*/
- WHERE a in (...) ORDER BY b,c /*对于排序来说,多个相等条件也是范围查询*/

5.3 案例实战
ORDER BY子句,尽量使用Index方式排序,避免使用FileSort方式排序。

执行案例前先清除student上的索引,只留主键:

DROP INDEX idx_age ON student;
DROP INDEX idx_age_classid_stuno ON student;
DROP INDEX idx_age_classid_name ON student;

#或者
call proc_drop_index(‘atguigudb2’,‘student’);
sql
DROP INDEX idx_age ON student;
DROP INDEX idx_age_classid_stuno ON student;
DROP INDEX idx_age_classid_name ON student;

#或者
call proc_drop_index(‘atguigudb2’,‘student’);

场景:查询年龄为30岁的,且学生编号小于101000的学生,按用户名称排序

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
sql
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;

image.png

查询结果如下:

mysql> SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
±--------±-------±-------±-----±--------+
| id | stuno | name | age | classId |
±--------±-------±-------±-----±--------+
| 922 | 100923 | elTLXD | 30 | 249 |
| 3723263 | 100412 | hKcjLb | 30 | 59 |
| 3724152 | 100827 | iHLJmh | 30 | 387 |
| 3724030 | 100776 | LgxWoD | 30 | 253 |
| 30 | 100031 | LZMOIa | 30 | 97 |
| 3722887 | 100237 | QzbJdx | 30 | 440 |
| 609 | 100610 | vbRimN | 30 | 481 |
| 139 | 100140 | ZqFbuR | 30 | 351 |
±--------±-------±-------±-----±--------+
8 rows in set, 1 warning (3.16 sec)
sql
mysql> SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
±--------±-------±-------±-----±--------+
| id | stuno | name | age | classId |
±--------±-------±-------±-----±--------+
| 922 | 100923 | elTLXD | 30 | 249 |
| 3723263 | 100412 | hKcjLb | 30 | 59 |
| 3724152 | 100827 | iHLJmh | 30 | 387 |
| 3724030 | 100776 | LgxWoD | 30 | 253 |
| 30 | 100031 | LZMOIa | 30 | 97 |
| 3722887 | 100237 | QzbJdx | 30 | 440 |
| 609 | 100610 | vbRimN | 30 | 481 |
| 139 | 100140 | ZqFbuR | 30 | 351 |
±--------±-------±-------±-----±--------+
8 rows in set, 1 warning (3.16 sec)

结论:type 是 ALL,即最坏的情况。Extra 里还出现了 Using filesort,也是最坏的情况。优化是必须的。

优化思路:

方案一: 为了去掉filesort我们可以把索引建成

#创建新索引
CREATE INDEX idx_age_name ON student(age,NAME);
sql
#创建新索引
CREATE INDEX idx_age_name ON student(age,NAME);

方案二: 尽量让where的过滤条件和排序使用上索引

建一个三个字段的组合索引:

DROP INDEX idx_age_name ON student;

CREATE INDEX idx_age_stuno_name ON student (age,stuno,NAME);

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
sql
DROP INDEX idx_age_name ON student;

CREATE INDEX idx_age_stuno_name ON student (age,stuno,NAME);

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;

mysql> SELECT SQL_NO_CACHE * FROM student
-> WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
±----±-------±-------±-----±--------+
| id | stuno | name | age | classId |
±----±-------±-------±-----±--------+
| 167 | 100168 | AClxEF | 30 | 319 |
| 323 | 100324 | bwbTpQ | 30 | 654 |
| 651 | 100652 | DRwIac | 30 | 997 |
| 517 | 100518 | HNSYqJ | 30 | 256 |
| 344 | 100345 | JuepiX | 30 | 329 |
| 905 | 100906 | JuWALd | 30 | 892 |
| 574 | 100575 | kbyqjX | 30 | 260 |
| 703 | 100704 | KJbprS | 30 | 594 |
| 723 | 100724 | OTdJkY | 30 | 236 |
| 656 | 100657 | Pfgqmj | 30 | 600 |
| 982 | 100983 | qywLqw | 30 | 837 |
| 468 | 100469 | sLEKQW | 30 | 346 |
| 988 | 100989 | UBYqJl | 30 | 457 |
| 173 | 100174 | UltkTN | 30 | 830 |
| 332 | 100333 | YjWiZw | 30 | 824 |
±----±-------±-------±-----±--------+
15 rows in set, 1 warning (0.00 sec)
sql
mysql> SELECT SQL_NO_CACHE * FROM student
-> WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
±----±-------±-------±-----±--------+
| id | stuno | name | age | classId |
±----±-------±-------±-----±--------+
| 167 | 100168 | AClxEF | 30 | 319 |
| 323 | 100324 | bwbTpQ | 30 | 654 |
| 651 | 100652 | DRwIac | 30 | 997 |
| 517 | 100518 | HNSYqJ | 30 | 256 |
| 344 | 100345 | JuepiX | 30 | 329 |
| 905 | 100906 | JuWALd | 30 | 892 |
| 574 | 100575 | kbyqjX | 30 | 260 |
| 703 | 100704 | KJbprS | 30 | 594 |
| 723 | 100724 | OTdJkY | 30 | 236 |
| 656 | 100657 | Pfgqmj | 30 | 600 |
| 982 | 100983 | qywLqw | 30 | 837 |
| 468 | 100469 | sLEKQW | 30 | 346 |
| 988 | 100989 | UBYqJl | 30 | 457 |
| 173 | 100174 | UltkTN | 30 | 830 |
| 332 | 100333 | YjWiZw | 30 | 824 |
±----±-------±-------±-----±--------+
15 rows in set, 1 warning (0.00 sec)

结果竟然有 filesort的 sql 运行速度, 超过了已经优化掉 filesort的 sql ,而且快了很多,几乎一瞬间
就出现了结果。

结论:

两个索引同时存在,mysql自动选择最优的方案。(对于这个例子,mysql选择
idx_age_stuno_name)。但是, 随着数据量的变化,选择的索引也会随之变化的 。
当【范围条件】和【group by 或者 order by】的字段出现二选一时,优先观察条件字段的过
滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段
上。反之,亦然。
思考:这里我们使用如下索引,是否可行?

DROP INDEX idx_age_stuno_name ON student;

CREATE INDEX idx_age_stuno ON student(age,stuno);
sql
DROP INDEX idx_age_stuno_name ON student;

CREATE INDEX idx_age_stuno ON student(age,stuno);

5.4 filesort算法:双路排序和单路排序
排序的字段若如果不在索引列上,则filesort会有两种算法:双路排序和单路排序

双路排序 (慢)
MySQL 4.1之前是使用双路排序 ,字面意思就是两次扫描磁盘,最终得到数据, 读取行指针和
order by列 ,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取
对应的数据输出

从磁盘取排序字段,在buffer进行排序,再从 磁盘取其他字段 。

取一批数据,要对磁盘进行两次扫描,众所周知,IO是很耗时的,所以在mysql4.1之后,出现了第二种
改进的算法,就是单路排序。

单路排序 (快)
从磁盘读取查询需要的 所有列 ,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输
出, 它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空
间, 因为它把每一行都保存在内存中了。

结论及引申出的问题
由于单路是后出的,总体而言好过双路
但是用单路有问题
在sort_buffer中,单路比多路要多占用很多空间,因为单路是把所有字段都取出,所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再取sort_buffer容量大小,再排…从而多次I/O。
单路本来想省一次lo操作,反而导致了大量的I/0操作,反而得不偿失。
优化策略
尝试提高 sort_buffer_size
image.png

尝试提高 max_length_for_sort_data
image.png

Order by 时select * 是一个大忌。最好只Query需要的字段。
image.png

GROUP BY优化

group by 使用索引的原则几乎跟order by一致 ,group by 即使没有过滤条件用到索引,也可以直接
使用索引。
group by 先排序再分组,遵照索引建的最佳左前缀法则
当无法使用索引列,增大 max_length_for_sort_data 和 sort_buffer_size 参数的设置
where效率高于having,能写在where限定的条件就不要写在having中了
减少使用order by,和业务沟通能不排序就不排序,或将排序放到程序端去做。Order by、group by、distinct这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。
包含了order by、group by、distinct这些查询的语句,where条件过滤出来的结果集请保持在1000行
以内,否则SQL会很慢。

优化分页查询

image.png

优化思路一
在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容。

EXPLAIN SELECT * FROM student t,(SELECT id FROM student ORDER BY id LIMIT 2000000,10) a WHERE t.id = a.id;
sql
EXPLAIN SELECT * FROM student t,(SELECT id FROM student ORDER BY id LIMIT 2000000,10) a WHERE t.id = a.id;

image.png

优化思路二
该方案适用于主键自增的表,可以把Limit 查询转换成某个位置的查询 。

EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10;
sql
EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10;

image.png

优先考虑覆盖索引

8.1 什么是覆盖索引?
理解方式一:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它
不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数
据,那就不需要读取行了。一个索引包含了满足查询结果的数据就叫做覆盖索引。

理解方式二:非聚簇复合索引的一种形式,它包括在查询里的SELECT、JOIN和WHERE子句用到的所有列
(即建索引的字段正好是覆盖查询条件中所涉及的字段)。

简单说就是, 索引列+主键 包含 SELECT 到 FROM之间查询的列 。

举例一:

#删除之前的索引

DROP INDEX idx_age_stuno ON student;

CREATE INDEX idx_age_name ON student ( age, NAME);

EXPLAIN SELECT * FROM student WHERE age <> 20;
sql
#删除之前的索引

DROP INDEX idx_age_stuno ON student;

CREATE INDEX idx_age_name ON student ( age, NAME);

EXPLAIN SELECT * FROM student WHERE age <> 20;

image.png

EXPLAIN SELECT id , age , NAME FROM student WHERE age <> 20 ;
sql
EXPLAIN SELECT id , age , NAME FROM student WHERE age <> 20 ;

image.png

上述都使用到了声明的索引,下面的情况则不然,在查询列中多了一列classid,显示未使用到索引:

EXPLAIN SELECT id , age , NAME,classid FROM student WHERE age <> 20;
sql
EXPLAIN SELECT id , age , NAME,classid FROM student WHERE age <> 20;

image.png

举例二:

EXPLAIN SELECT * FROM student WHERE NAME LIKE '%abc ’ ;
sql
EXPLAIN SELECT * FROM student WHERE NAME LIKE '%abc ’ ;

image.png

CREATE INDEX idx_age_name ON student ( age , NAME);

EXPLAIN SELECT id , age ,NAME FROM student WHERE NAME LIKE '%abc ’ ;
sql
CREATE INDEX idx_age_name ON student ( age , NAME);

EXPLAIN SELECT id , age ,NAME FROM student WHERE NAME LIKE '%abc ’ ;

image.png

上述都使用到了声明的索引,下面的情况则不然,查询列依然多了classid,结果是未使用到索引:

EXPLAIN SELECT id , age ,NAME, classid FROM student WHERE NAME LIKE‘%ab;
sql
EXPLAIN SELECT id , age ,NAME, classid FROM student WHERE NAME LIKE‘%ab;

image.png

8.2 覆盖索引的利弊
好处:
避免Innodb表进行索引的二次查询(回表)
Innodb是以聚集索引的顺序来存储的,对于Innodb来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据,在查找到相应的键值后,还需通过主键进行二次查询才能获取我们真实所需要的数据。

在覆盖索引中,二级索引的键值中可以获取所要的数据,避免了对主键的二次查询,减少了I0操作,提升了查询效率。

可以把随机IO变成顺序IO加快查询效率
由于覆盖索引是按键值的顺序存储的,对于I0密集型的范围查找来说,对比随机从磁盘读取每一行的数据Io要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的IO转变成索引查找的顺序I0。

由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。

弊端:
索引字段的维护 总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这是业务
DBA,或者称为业务数据架构师的工作。

如何给字符串添加索引

有一张教师表,表定义如下:

create table teacher(
ID bigint unsigned primary key,
email varchar(64),

)engine=innodb;
sql
create table teacher(
ID bigint unsigned primary key,
email varchar(64),

)engine=innodb;

讲师要使用邮箱登录,所以业务代码中一定会出现类似于这样的语句:

mysql> select col1, col2 from teacher where email=‘xxx’;
sql
mysql> select col1, col2 from teacher where email=‘xxx’;

如果email这个字段上没有索引,那么这个语句就只能做 全表扫描 。

9.1 前缀索引
MySQL是支持前缀索引的。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字
符串。

mysql> alter table teacher add index index1(email);

#或

mysql> alter table teacher add index index2(email(6));
sql
mysql> alter table teacher add index index1(email);

#或

mysql> alter table teacher add index index2(email(6));

这两种不同的定义在数据结构和存储上有什么区别呢?下图就是这两个索引的示意图。

image.png

以及

image.png

如果使用的是index1(即email整个字符串的索引结构),执行顺序是这样的:

从index1索引树找到满足索引值是’ zhangssxyz@xxx.com ’的这条记录,取得ID2的值;

到主键上查到主键值是ID2的行,判断email的值是正确的,将这行记录加入结果集;

取index1索引树上刚刚查到的位置的下一条记录,发现已经不满足email=’ zhangssxyz@xxx.com ’的
条件了,循环结束。

这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行。

如果使用的是index2(即email(6)索引结构),执行顺序是这样的:

从index2索引树找到满足索引值是’zhangs’的记录,找到的第一个是ID1;

到主键上查到主键值是ID1的行,判断出email的值不是’ zhangssxyz@xxx.com ’,这行记录丢弃;

取index2上刚刚查到的位置的下一条记录,发现仍然是’zhangs’,取出ID2,再到ID索引上取整行然
后判断,这次值对了,将这行记录加入结果集;

重复上一步,直到在idxe2上取到的值不是’zhangs’时,循环结束。

也就是说使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。前面
已经讲过区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。

9.2 前缀索引对覆盖索引的影响
结论:
使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考
虑的一个因素。

索引下推

Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优
化方式。ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。

10.1 使用前后对比
Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的优化方式。

如果没有ICP,存储引擎会遍历索引以定位基表中的行,并将它们返回给MySQL服务器,由MySQL服务器评估WHERE后面的条件是否保留行。

启用ICP后,如果部分WHERE条件可以仅使用索引中的列进行筛选,则mysql服务器会把这部分WHERE条件放到存储引擎筛选。然后,存储引擎通过使用索引条目来筛选数据,并且只有在满足这一条件时才从表中读取行。

好处:ICP可以减少存储引擎必须访问基表的次数和MySQL服务器必须访问存储引擎的次数。
但是,ICP的加速效果取决于在存储引擎内通过ICP筛选掉的数据的比例。
10.2 ICP的使用条件
ICP的使用条件:
① 只能用于二级索引(secondary index)

②explain显示的执行计划中type值(join 类型)为 range 、 ref 、 eq_ref 或者 ref_or_null 。

③ 并非全部where条件都可以用ICP筛选,如果where条件的字段不在索引列中,还是要读取整表的记录
到server端做where过滤。

④ ICP可以用于MyISAM和InnnoDB存储引擎

⑤ MySQL 5.6版本的不支持分区表的ICP功能,5.7版本的开始支持。

⑥ 当SQL使用覆盖索引时,不支持ICP优化方法。

10.3 ICP使用案例
案例1

SELECT * FROM tuser
WHERE NAME LIKE ‘张%’
AND age = 10 AND ismale = 1;
sql
SELECT * FROM tuser
WHERE NAME LIKE ‘张%’
AND age = 10 AND ismale = 1;

image.png

案例2

image.png

普通索引 vs 唯一索引

从性能的角度考虑,你选择唯一索引还是普通索引呢?选择的依据是什么呢?

假设,我们有一个主键列为ID的表,表中有字段k,并且在k上有索引,假设字段 k 上的值都不重复。

这个表的建表语句是:

mysql> create table test(
id int primary key,
k int not null,
name varchar(16),
index (k)
)engine=InnoDB;
sql
mysql> create table test(
id int primary key,
k int not null,
name varchar(16),
index (k)
)engine=InnoDB;

表中R1~R5的(ID,k)值分别为(100,1)、(200,2)、(300,3)、(500,5)和(600,6)。

11.1 查询过程
假设,执行查询的语句是 select id from test where k=5。

对于普通索引来说,查找到满足条件的第一个记录(5,500)后,需要查找下一个记录,直到碰到第一
个不满足k=5条件的记录。

对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检
索。

那么,这个不同带来的性能差距会有多少呢?答案是, 微乎其微 。

11.2 更新过程
为了说明普通索引和唯一索引对更新语句性能的影响这个问题,介绍一下change buffer。

当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下, InooDB会将这些更新操作缓存在change buffer中 ,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。

将change buffer中的操作应用到原数据页,得到最新结果的过程称为 merge 。除了 访问这个数据页 会触发merge外,系统有 后台线程会定期 merge。在 数据库正常关闭(shutdown) 的过程中,也会执行merge操作。

如果能够将更新操作先记录在change buffer, 减少读磁盘 ,语句的执行速度会得到明显的提升。而且,
数据读入内存是需要占用 buffer pool 的,所以这种方式还能够 避免占用内存 ,提高内存利用率。

唯一索引的更新就不能使用change buffer ,实际上也只有普通索引可以使用。

如果要在这张表中插入一个新记录(4,400)的话,InnoDB的处理流程是怎样的?

11.3 change buffer的使用场景
普通索引和唯一索引应该怎么选择?其实,这两类索引在查询能力上是没差别的,主要考虑的是对 更新性能 的影响。所以,建议你 尽量选择普通索引 。

在实际使用中会发现, 普通索引 和 change buffer 的配合使用,对于 数据量大 的表的更新优化还是很明显的。

如果所有的更新后面,都马上 伴随着对这个记录的查询 ,那么你应该 关闭change buffer 。而在其他情况下,change buffer都能提升更新性能。

由于唯一索引用不上change buffer的优化机制,因此如果 业务可以接受 ,从性能角度出发建议优先考虑非唯一索引。但是如果"业务可能无法确保"的情况下,怎么处理呢?

首先, 业务正确性优先 。我们的前提是“业务代码已经保证不会写入重复数据”的情况下,讨论性能问题。如果业务不能保证,或者业务就是要求数据库来做约束,那么没得选,必须创建唯一索引。这种情况下,本节的意义在于,如果碰上了大量插入数据慢、内存命中率低的时候,给你多提供一个排查思路。

然后,在一些“ 归档库 ”的场景,你是可以考虑使用唯一索引的。比如,线上数据只需要保留半年,然后历史数据保存在归档库。这时候,归档数据已经是确保没有唯一键冲突了。要提高归档效率,可以考虑把表里面的唯一索引改成普通索引。

其它查询优化策略

12.1 EXISTS 和 IN 的区分
问题:

不太理解哪种情况下应该使用 EXISTS,哪种情况应该用 IN。选择的标准是看能否使用表的索引吗?

回答:

索引是个前提,其实选择与否还是要看表的大小。你可以将选择的标准理解为小表驱动大表。在这种方式下效率是最高的。

比如下面这样:

SELECT * FROM A WHERE cc IN (SELECT cc FROM B)
SELECT * FROM A WHERE EXISTS (SELECT cc FROM B WHERE B.cc=A.cc)
sql
SELECT * FROM A WHERE cc IN (SELECT cc FROM B)
SELECT * FROM A WHERE EXISTS (SELECT cc FROM B WHERE B.cc=A.cc)

当A小于B时,用EXISTS。因为EXISTS的实现,相当于外表循环,实现的逻辑类似于:

for i in A
for j in B
if j.cc == i.cc then

java
for i in A
for j in B
if j.cc == i.cc then

当B小于A时用IN,因为实现的逻辑类似于:

for i in B
for j in A
if j.cc == i.cc then …

java
for i in B
for j in A
if j.cc == i.cc then …

哪个表小就用哪个表来驱动,A表小就用EXISTS,B表小就用IN。

12.2 COUNT()与COUNT(具体字段)效率
问:在 MySQL 中统计数据表的行数,可以使用三种方式: SELECT COUNT(
) 、 SELECT COUNT(1) 和 SELECT COUNT(具体字段) ,使用这三者之间的查询效率是怎样的?

答:

前提:如果你要统计的是某个字段的非空数据行数,则另当别论,毕竟比较执行效率的前提是结果一样才可以。

环节1: COUNT()和COUNT(1)都是对所有结果进行COUNT,COUNT()和COUNT(1)本质上并没有区别(二者执行时间可能略有差别,不过你还是可以把它俩的执行效率看成是相等的)。如果有WHERE子句,则是对所有符合筛选条件的数据行进行统计;如果没有WHERE子句,则是对数据表的数据行数进行统计。

环节2:如果是MyISAM存储引擎,统计数据表的行数只需要O(1)的复杂度,这是因为每张MyISAM的数据表都有一个meta 信息存储了row_count值,而一致性则由表级锁来保证。

如果是InnoDB存储引擎,因为InnoDB支持事务,采用行级锁和MVCC机制,所以无法像MyISAM一样,维护一个row_count变量,因此需要采用扫描全表,进行循环+计数的方式来完成统计,是O(N)级别复杂度。

环节3:在InnoDB引擎中,如果采用COUNT(具体字段)来统计数据行数,要尽量采用二级索引。因为主键采用的索引是聚簇索引,聚簇索引包含的信息多,明显会大于二级索引(非聚簇索引)。对于COUNT(*)和COUNT(1)来说,它们不需要查找具体的行,只是统计行数,系统会自动采用占用空间更小的二级索引来进行统计。

如果有多个二级索引,会使用key_len 小的二级索引进行扫描。当没有二级索引的时候,才会采用主键索引来进行统计。

12.3 关于SELECT(*)
在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,推荐使用SELECT <字段列表> 查询。原因:

① MySQL 在解析的过程中,会通过 查询数据字典 将"*"按序转换成所有列名,这会大大的耗费资源和时间。

② 无法使用 覆盖索引

12.4 LIMIT 1 对优化的影响
针对的是会扫描全表的 SQL 语句,如果你可以确定结果集只有一条,那么加上 LIMIT 1 的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。

如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上 LIMIT 1 了。

12.5 多使用COMMIT
只要有可能,在程序中尽量多使用 COMMIT,这样程序的性能得到提高,需求也会因为 COMMIT 所释放的资源而减少。

COMMIT 所释放的资源:

回滚段上用于恢复数据的信息
被程序语句获得的锁
redo / undo log buffer 中的空间
管理上述 3 种资源中的内部花费

淘宝数据库,主键如何设计的?

聊一个实际问题:淘宝的数据库,主键是如何设计的?

某些错的离谱的答案还在网上年复一年的流传着,甚至还成为了所谓的MySQL军规。其中,一个最明显的错误就是关于MySQL的主键设计。

大部分人的回答如此自信:用8字节的 BIGINT 做主键,而不要用INT。 错 !

这样的回答,只站在了数据库这一层,而没有 从业务的角度 思考主键。主键就是一个自增ID吗?站在
2022年的新年档口,用自增做主键,架构设计上可能 连及格都拿不到 。

13.1 自增ID的问题
自增ID做主键,简单易懂,几乎所有数据库都支持自增类型,只是实现上各自有所不同而已。自增ID除了简单,其他都是缺点,总体来看存在以下几方面的问题:

  1. 可靠性不高

存在自增ID回溯的问题,这个问题直到最新版本的MySQL 8.0才修复。

  1. 安全性不高

对外暴露的接口可以非常容易猜测对应的信息。比如:/User/1/这样的接口,可以非常容易猜测用户ID的值为多少,总用户数量有多少,也可以非常容易地通过接口进行数据的爬取。

  1. 性能差

自增ID的性能较差,需要在数据库服务器端生成。

  1. 交互多

业务还需要额外执行一次类似 last_insert_id() 的函数才能知道刚才插入的自增值,这需要多一次的网络交互。在海量并发的系统中,多1条SQL,就多一次性能上的开销。

  1. 局部唯一性

最重要的一点,自增ID是局部唯一,只在当前数据库实例中唯一,而不是全局唯一,在任意服务器间都是唯一的。对于目前分布式系统来说,这简直就是噩梦。

13.2 业务字段做主键
为了能够唯一地标识一个会员的信息,需要为 会员信息表 设置一个主键。那么,怎么为这个表设置主键,才能达到我们理想的目标呢? 这里我们考虑业务字段做主键。

表数据如下:

image.png

在这个表里,哪个字段比较合适呢?

选择卡号(cardno)
会员卡号(cardno)看起来比较合适,因为会员卡号不能为空,而且有唯一性,可以用来 标识一条会员
记录。

mysql> CREATE TABLE demo.membermaster
-> (
-> cardno CHAR(8) PRIMARY KEY, – 会员卡号为主键
-> membername TEXT,
-> memberphone TEXT,
-> memberpid TEXT,
-> memberaddress TEXT,
-> sex TEXT,
-> birthday DATETIME
-> );
Query OK, 0 rows affected (0.06 sec)
sql
mysql> CREATE TABLE demo.membermaster
-> (
-> cardno CHAR(8) PRIMARY KEY, – 会员卡号为主键
-> membername TEXT,
-> memberphone TEXT,
-> memberpid TEXT,
-> memberaddress TEXT,
-> sex TEXT,
-> birthday DATETIME
-> );
Query OK, 0 rows affected (0.06 sec)

不同的会员卡号对应不同的会员,字段“cardno”唯一地标识某一个会员。如果都是这样,会员卡号与会员一一对应,系统是可以正常运行的。

但实际情况是, 会员卡号可能存在重复使用 的情况。比如,张三因为工作变动搬离了原来的地址,不再到商家的门店消费了 (退还了会员卡),于是张三就不再是这个商家门店的会员了。但是,商家不想让这个会 员卡空着,就把卡号是“10000001”的会员卡发给了王五。

从系统设计的角度看,这个变化只是修改了会员信息表中的卡号是“10000001”这个会员 信息,并不会影响到数据一致性。也就是说,修改会员卡号是“10000001”的会员信息, 系统的各个模块,都会获取到修改后的会员信息,不会出现“有的模块获取到修改之前的会员信息,有的模块获取到修改后的会员信息,而导致系统内部数据不一致”的情况。因此,从 信息系统层面 上看是没问题的。

但是从使用 系统的业务层面 来看,就有很大的问题 了,会对商家造成影响。

比如,我们有一个销售流水表(trans),记录了所有的销售流水明细。2020 年 12 月 01 日,张三在门店购买了一本书,消费了 89 元。那么,系统中就有了张三买书的流水记录,如下所示:

image.png

接着,我们查询一下 2020 年 12 月 01 日的会员销售记录:

mysql> SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-> FROM demo.trans AS a
-> JOIN demo.membermaster AS b
-> JOIN demo.goodsmaster AS c
-> ON (a.cardno = b.cardno AND a.itemnumber=c.itemnumber);
±-----------±----------±---------±-----------±--------------------+
| membername | goodsname | quantity | salesvalue | transdate |
±-----------±----------±---------±-----------±--------------------+
| 张三 | 书 | 1.000 | 89.00 | 2020-12-01 00:00:00 |
±-----------±----------±---------±-----------±--------------------+
1 row in set (0.00 sec)
sql
mysql> SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-> FROM demo.trans AS a
-> JOIN demo.membermaster AS b
-> JOIN demo.goodsmaster AS c
-> ON (a.cardno = b.cardno AND a.itemnumber=c.itemnumber);
±-----------±----------±---------±-----------±--------------------+
| membername | goodsname | quantity | salesvalue | transdate |
±-----------±----------±---------±-----------±--------------------+
| 张三 | 书 | 1.000 | 89.00 | 2020-12-01 00:00:00 |
±-----------±----------±---------±-----------±--------------------+
1 row in set (0.00 sec)

如果会员卡“10000001”又发给了王五,我们会更改会员信息表。导致查询时:

mysql> SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-> FROM demo.trans AS a
-> JOIN demo.membermaster AS b
-> JOIN demo.goodsmaster AS c
-> ON (a.cardno = b.cardno AND a.itemnumber=c.itemnumber);
±-----------±----------±---------±-----------±--------------------+
| membername | goodsname | quantity | salesvalue | transdate |
±-----------±----------±---------±-----------±--------------------+
| 王五 | 书 | 1.000 | 89.00 | 2020-12-01 00:00:00 |
±-----------±----------±---------±-----------±--------------------+
1 row in set (0.01 sec)
sql
mysql> SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-> FROM demo.trans AS a
-> JOIN demo.membermaster AS b
-> JOIN demo.goodsmaster AS c
-> ON (a.cardno = b.cardno AND a.itemnumber=c.itemnumber);
±-----------±----------±---------±-----------±--------------------+
| membername | goodsname | quantity | salesvalue | transdate |
±-----------±----------±---------±-----------±--------------------+
| 王五 | 书 | 1.000 | 89.00 | 2020-12-01 00:00:00 |
±-----------±----------±---------±-----------±--------------------+
1 row in set (0.01 sec)

这次得到的结果是:王五在 2020 年 12 月 01 日,买了一本书,消费 89 元。显然是错误的!结论:千万不能把会员卡号当做主键。

选择会员电话 或 身份证号

会员电话可以做主键吗?不行的。在实际操作中,手机号也存在 被运营商收回 ,重新发给别人用的情况。

那身份证号行不行呢?好像可以。因为身份证决不会重复,身份证号与一个人存在一一对 应的关系。可问题是,身份证号属于 个人隐私 ,顾客不一定愿意给你。要是强制要求会员必须登记身份证号,会把很多客人赶跑的。其实,客户电话也有这个问题,这也是我们在设计会员信息表的时候,允许身份证号和电话都为空的原因。

所以,建议尽量不要用跟业务有关的字段做主键。毕竟,作为项目设计的技术人员,我们谁也无法预测
在项目的整个生命周期中,哪个业务字段会因为项目的业务需求而有重复,或者重用之类的情况出现。

经验:
刚开始使用 MySQL 时,很多人都很容易犯的错误是喜欢用业务字段做主键,想当然地认为了解业
务需求,但实际情况往往出乎意料,而更改主键设置的成本非常高。

13.3 淘宝的主键设计
在淘宝的电商业务中,订单服务是一个核心业务。请问, 订单表的主键 淘宝是如何设计的呢?是自增ID吗?

打开淘宝,看一下订单信息:

image.png

从上图可以发现,订单号不是自增ID!我们详细看下上述4个订单号:

1550672064762308113
1481195847180308113
1431156171142308113
1431146631521308113
bash
1550672064762308113
1481195847180308113
1431156171142308113
1431146631521308113

订单号是19位的长度,且订单的最后5位都是一样的,都是08113。且订单号的前面14位部分是单调递增的。

大胆猜测,淘宝的订单ID设计应该是:

订单ID = 时间 + 去重字段 + 用户ID后6位尾号
bash
订单ID = 时间 + 去重字段 + 用户ID后6位尾号

这样的设计能做到全局唯一,且对分布式系统查询及其友好。粗体

13.4 推荐的主键设计
非核心业务 :对应表的主键自增ID,如告警、日志、监控等信息。

核心业务 :主键设计至少应该是全局唯一且是单调递增。全局唯一保证在各系统之间都是唯一的,单调递增是希望插入时不影响数据库性能。

这里推荐最简单的一种主键设计:UUID。

UUID的特点:
全局唯一,占用36字节,数据无序,插入性能差。

认识UUID:
为什么UUID是全局唯一的?

为什么UUID占用36个字节?

为什么UUID是无序的?

MySQL数据库的UUID组成如下所示:

UUID = 时间+UUID版本(16字节)- 时钟序列(4字节) - MAC地址(12字节)
sql
UUID = 时间+UUID版本(16字节)- 时钟序列(4字节) - MAC地址(12字节)

我们以UUID值e0ea12d4-6473-11eb-943c-00155dbaa39d举例:

image.png

为什么UUID是全局唯一的?
在UUID中时间部分占用60位,存储的类似TIMESTAMP的时间戳,但表示的是从1582-10-15 00:00:00.00
到现在的100ns的计数。可以看到UUID存储的时间精度比TIMESTAMPE更高,时间维度发生重复的概率降
低到1/100ns。

时钟序列是为了避免时钟被回拨导致产生时间重复的可能性。MAC地址用于全局唯一。

为什么UUID占用36个字节?
UUID根据字符串进行存储,设计时还带有无用"-"字符串,因此总共需要36个字节。

为什么UUID是随机无序的呢?
因为UUID的设计中,将时间低位放在最前面,而这部分的数据是一直在变化的,并且是无序。

改造UUID
若将时间高低位互换,则时间就是单调递增的了,也就变得单调递增了。MySQL 8.0可以更换时间低位和
时间高位的存储方式,这样UUID就是有序的UUID了。

MySQL 8.0还解决了UUID存在的空间占用的问题,除去了UUID字符串中无意义的"-"字符串,并且将字符
串用二进制类型保存,这样存储空间降低为了16字节。

可以通过MySQL8.0提供的uuid_to_bin函数实现上述功能,同样的,MySQL也提供了bin_to_uuid函数进行
转化:

SET @uuid = UUID();

SELECT @uuid,uuid_to_bin(@uuid),uuid_to_bin(@uuid,TRUE);
sql
SET @uuid = UUID();

SELECT @uuid,uuid_to_bin(@uuid),uuid_to_bin(@uuid,TRUE);

image.png

通过函数uuid_to_bin(@uuid,true)将UUID转化为有序UUID了。全局唯一 + 单调递增,这不就是我们想要
的主键!

4、有序UUID性能测试
16字节的有序UUID,相比之前8字节的自增ID,性能和存储空间对比究竟如何呢?

我们来做一个测试,插入1亿条数据,每条数据占用500字节,含有3个二级索引,最终的结果如下所示:

image.png

从上图可以看到插入1亿条数据有序UUID是最快的,而且在实际业务使用中有序UUID在 业务端就可以生 成 。还可以进一步减少SQL的交互次数。

另外,虽然有序UUID相比自增ID多了8个字节,但实际只增大了3G的存储空间,还可以接受。

在当今的互联网环境中,非常不推荐自增ID作为主键的数据库设计。更推荐类似有序UUID的全局
唯一的实现。
另外在真实的业务系统中,主键还可以加入业务和系统属性,如用户的尾号,机房的信息等。这样
的主键设计就更为考验架构师的水平了。

如果不是MySQL8.0 肿么办?
手动赋值字段做主键!

比如,设计各个分店的会员表的主键,因为如果每台机器各自产生的数据需要合并,就可能会出现主键
重复的问题。

可以在总部 MySQL 数据库中,有一个管理信息表,在这个表中添加一个字段,专门用来记录当前会员编
号的最大值。

门店在添加会员的时候,先到总部 MySQL 数据库中获取这个最大值,在这个基础上加 1,然后用这个值
作为新会员的“id”,同时,更新总部 MySQL 数据库管理信息表中的当 前会员编号的最大值。

这样一来,各个门店添加会员的时候,都对同一个总部 MySQL 数据库中的数据表字段进 行操作,就解
决了各门店添加会员时会员编号冲突的问题。

数据库优化

数据库调优的措施

调优的目标

尽可能 节省系统资源 ,以便系统可以提供更大负荷的服务。(吞吐量更大)
合理的结构设计和参数调整,以提高用户操作 响应的速度 。(响应速度更快)
减少系统的瓶颈,提高MySQL数据库整体的性能。

如何定位调优问题

不过随着用户量的不断增加,以及应用程序复杂度的提升,我们很难用“更快”去定义数据库调优的目标,因为用户在不同时间段访问服务器遇到的瓶颈不同,比如双十一促销的时候会带来大规模的并发访问;还有用户在进行不同业务操作的时候,数据库的事务处理和 SQL查询都会有所不同。因此我们还需要更加精细的定位,去确定调优的目标。

如何确定呢?一般情况下,有如下几种方式:

用户的反馈(主要)
用户是我们的服务对象,因此他们的反馈是最直接的。虽然他们不会直接提出技术建议,但是有些问题往往是用户第一时间发现的。我们要重视用户的反馈,找到和数据相关的问题。

日志分析(主要)
我们可以通过查看数据库日志和操作系统日志等方式找出异常情况,通过它们来定位遇到的问题。

服务器资源使用监控
通过监控服务器的cPU、内存、Io等使用情况,可以实时了解服务器的性能使用,与历史情况进行对比

数据库内部状况监控
在数据库的监控中,活动会话(Active Session)监控是一个重要的指标。通过它,你可以清楚地了解数据库当前是否处于非常繁忙的状态,是否存在SQL堆积等。

其它
除了活动会话监控以外,我们也可以对 事务 、 锁等待 等进行监控,这些都可以帮助我们对数据库的运
行状态有更全面的认识。

调优的维度和步骤

我们需要调优的对象是整个数据库管理系统,它不仅包括 SQL 查询,还包括数据库的部署配置、架构
等。从这个角度来说,我们思考的维度就不仅仅局限在 SQL 优化上了。通过如下的步骤我们进行梳理:

第1步:选择适合的 DBMS
如果对事务性处理以及安全性要求高的话,可以选择商业的数据库产品。这些数据库在事务处理和查询性能上都比较强,比如采用SQL Server、Oracle,那么单表存储上亿条数据是没有问题的。如果数据表设计得好,即使不采用分库分表的方式,查询效率也不差。

除此以外,你也可以采用开源的MysQL进行存储,它有很多存储引擎可以选择,如果进行事务处理的话可以选择lnnoDB,非事务处理可以选择MylSAM。

NoSQL_阵营包括键值型数据库、文档型数据库、搜索引擎、列式存储和图形数据库。这些数据库的优缺点和使用场景各有不同,比如列式存储数据库可以大幅度降低系统的I/o,适合于分布式文件系统,但如果数据需要频地增删改,那么列式存储就不太适用了。

DBMS的选择关系到了后面的整个设计过程,所以第一步就是要选择适合的DBMS。如果已经确定好了DBMS
那么这步可以跳过。|

第2步:优化表设计
选择了DBMS之后,我们就需要进行表设计了。而数据表的设计方式也直接影响了后续的SQL查询语句。RDBMS中,每个对象都可以定义为一张表,表与表之间的关系代表了对象之间的关系。如果用的是MySQL,我们还可以根据不同表的使用需求,选择不同的存储引擎。除此以外,还有一些优化的原则可以参考:

表结构要尽量遵循三范式的原则。这样可以让数据结构更加清晰规范,减少冗余字段,同时也减少了在更新,插入和删除数据时等异常情况的发生。
如果查询应用比较多,尤其是需要进行多表联查的时候,可以采用反范式进行优化。反范式采用空间换时 间的方式,通过增加冗余字段提高查询的效率。
表字段的数据类型选择,关系到了查询效率的高低以及存储空间的大小。一般来说,如果字段可以采用数值类型就不要采用字符类型;字符长度要尽可能设计得短一些。针对字符类型来说,当确定字符长度固定时就可以采用CHAR类型;当长度不固定时,通常采用VARCHAR类型。
数据表的结构设计很基础,也很关键。好的表结构可以在业务发展和用户量增加的情况下依然发挥作用,不好的表结构设计会让数据表变得非常臃肿,查询效率也会降低。

第3步:优化逻辑查询
当我们建立好数据表之后,就可以对数据表进行增删改查的操作了。这时我们首先需要考虑的是逻辑查询优化。SQL查询优化,可以分为逻辑查询优化和物理查询优化。逻辑查询优化就是通过改变SQL语句的内容让SQL执行效率更高效,采用的方式是对SQL语句进行等价变换,对查询进行重写。

SQL的查询重写包括了子查询优化、等价谓词重写、视图重写、条件简化、连接消除和嵌套连接消除等。

比如我们在讲解EXISTS子查询和IN子查询的时候,会根据小表驱动大表的原则选择适合的子查询。在WHERE子句中会尽量避免对字段进行函数运算,它们会让字段的索引失效。

举例:查询评论内容开头为abc的内容都有哪些,如果在WHERE子句中使用了函数,语句就会写成下面这样

SELECT comment_id,comment_text,comment_time FROM product_comment 
WHERE SUBSTRING ( comment_text, 1,3)= 'abc '

采用查询重写的方式进行等价替换:

SELECT comment_id,comment_text,comment_time FROM product_comment 
WHERE comment_text LIKE 'abc%'

第4步:优化物理查询
物理查询优化是在确定了逻辑查询优化之后,采用物理优化技术(比如索引等),通过计算代价模型对
各种可能的访问路径进行估算,从而找到执行方式中代价最小的作为执行计划。在这个部分中,我们需
要掌握的重点是对索引的创建和使用。

但索引不是万能的,我们需要根据实际情况来创建索引。那么都有哪些情况需要考虑呢?我们在前面几章中已经进行了细致的剖析。

SQL查询时需要对不同的数据表进行查询,因此在物理查询优化阶段也需要确定这些查询所采用的路径,具体的情况包括:

单表扫描:对于单表扫描来说,我们可以全表扫描所有的数据,也可以局部扫描。

两张表的连接:常用的连接方式包括了嵌套循环连接、HASH 连接和合并连接。

多张表的连接∶多张数据表进行连接的时候,顺序很重要,因为不同的连接路径查询的效率不同,搜索空间也会不同。我们在进行多表连接的时候,搜索空间可能会达到很高的数据量级,巨大的搜索空间显然会占用更多的资源,因此我们需要通过调整连接顺序,将搜索空间调整在一个可接受的范围内。

第5步:使用 Redis 或 Memcached 作为缓存
除了可以对 SQL 本身进行优化以外,我们还可以请外援提升查询的效率。

因为数据都是存放到数据库中,我们需要从数据库层中取出数据放到内存中进行业务逻辑的操作,当用
户量增大的时候,如果频繁地进行数据查询,会消耗数据库的很多资源。如果我们将常用的数据直接放
到内存中,就会大幅提升查询的效率。

键值存储数据库可以帮我们解决这个问题。

常用的键值存储数据库有 Redis 和 Memcached,它们都可以将数据存放到内存中。

从可靠性来说, Redis支持持久化,可以让我们的数据保存在硬盘上,不过这样一来性能消耗也会比较大。
而Memcached 仅仅是内存存储,不支持持久化。

从支持的数据类型来说,Redis 比Memcached要多,它不仅支持 key-value类型的数据,还支持List,Set,Hash等数据结构。当我们有持久化需求或者是更高级的数据处理需求的时候,就可以使用Redis。如果是简单的key-value存储,则可以使用Memcached。

通常我们对于查询响应要求高的场景(响应时间短,吞吐量大),可以考虑内存数据库,毕竟术业有专攻。传统的 RDBMS都是将数据存储在硬盘上,而内存数据库则存放在内存中,查询起来要快得多。不过使用不同的工具,也增加了开发人员的使用成本。

第6步:库级优化
库级优化是站在数据库的维度上进行的优化策略,比如控制一个库中的数据表数量。另外,单一的数据库总会遇到各种限制,不如取长补短,利用"外援的方式。通过主从架构优化我们的读写策略,通过对数据库进行垂直或者水平切分,突破单一数据库或数据表的访问限制,提升查询的性能。

1、读写分离
如果读和写的业务量都很大,并且它们都在同一个数据库服务器中进行操作,那么数据库的性能就会出现瓶颈,这时为了提升系统的性能,优化用户体验,我们可以采用读写分离的方式降低主数据库的负载,比如用主数据库( master)完成写操作,用从数据库(slave)完成读操作。

2、数据分片
对数据库分库分表。当数据量级达到千万级以上时,有时候我们需要把一个数据库切成多份,放到不同的数据库服务器上,减少对单一数据库服务器的访问压力。如果你使用的是MySQL,就可以使用MysQL自带的分区表功能,当然你也可以考虑自己做垂直拆分(分库)、水平拆分(分表)、垂直+水平拆分(分库分表)。

但需要注意的是,分拆在提升数据库性能的同时,也会增加维护和使用成本。

优化MySQL服务器

优化MysQL服务器主要从两个方面来优化,一方面是对硬件进行优化;另一方面是对MySQL服务的参数进行优化。这部分的内容需要较全面的知识,一般只有专业的数据库管理员才能进行这一类的优化。对于可以定制参数的操作系统,也可以针对MySQL进行操作系统优化。

优化服务器硬件

服务器的硬件性能直接决定着MySQL数据库的性能。硬件的性能瓶颈直接决定MySQL数据库的运行速度
和效率。针对性能瓶颈提高硬件配置,可以提高MySQL数据库查询、更新的速度。

(1)配置较大的内存。足够大的内存是提高MysQL数据库性能的方法之一。内存的速度比磁盘I/O快得多,可以通过增加系统的缓冲区容量使数据在内存中停留的时间更长,以减少磁盘I/0。

(2)配置高速磁盘系统,以减少读盘的等待时间,提高响应速度。磁盘的I/O能力,也就是它的寻道能力,目前的SCSI高速旋转的是7200转/分钟,这样的速度,一旦访问的用户量上去,磁盘的压力就会过大,如果是每天的网站pv(page view)在150w,这样的一般的配置就无法满足这样的需求了。现在SSD盛行,在SSD上随机访问和顺序访问性能几乎差不多,使用SSD可以减少随机Io带来的性能损耗。

(3)合理分布磁盘I/0,把磁盘I/0分散在多个设备上,以减少资源竞争,提高并行操作能力。

(4)配置多处理器,MySQL是多线程的数据库,多处理器可同时执行多个线程。

优化MySQL的参数

通过优化MySQL的参数可以提高资源利用率,从而达到提高MySQL服务器性能的目的。

MySQL服务的配置参数都在my.cnf或者my.ini文件的[mysqld]组中。配置完参数以后,需要重新启动MySQL服务才会生效。
下面对几个对性能影响比较大的参数进行详细介绍。

innodb_buffer_pool_size :这个参数是Mysql数据库最重要的参数之一,表示InnoDB类型的表和索引的最大缓存 。它不仅仅缓存 索引数据 ,还会缓存 表的数据 。这个值越大,查询的速度就会越快。但是这个值太大会影响操作系统的性能。

key_buffer_size :表示 索引缓冲区的大小 。索引缓冲区是所有的线程共享 。增加索引缓冲区可以得到更好处理的索引(对所有读和多重写)。当然,这个值不是越大越好,它的大小取决于内存的大小。如果这个值太大,就会导致操作系统频繁换页,也会降低系统性能。对于内存在 4GB 左右的服务器该参数可设置为 256M 或 384M 。

table_cache :表示同时打开的表的个数 。这个值越大,能够同时打开的表的个数越多。物理内存越大,设置就越大。默认为2402,调到512-1024最佳。这个值不是越大越好,因为同时打开的表太多会影响操作系统的性能。

query_cache_size :表示查询缓冲区的大小 。可以通过在MySQL控制台观察,如果Qcache_lowmem_prunes的值非常大,则表明经常出现缓冲不够的情况,就要增加Query_cache_size的值;如果Qcache_hits的值非常大,则表明查询缓冲使用非常频繁,如果该值较小反而会影响效率,那么可以考虑不用查询缓存;Qcache_free_blocks,如果该值非常大,则表明缓冲区中碎片很多。MySQL8.0之后失效。该参数需要和query_cache_type配合使用。

query_cache_type 的值是0时,所有的查询都不使用查询缓存区。但是query_cache_type=0并不会导致MySQL释放query_cache_size所配置的缓存区内存。当query_cache_type=1时,所有的查询都将使用查询缓存区,除非在查询语句中指定SQL_NO_CACHE ,如SELECT SQL_NO_CACHE * FROM tbl_name。 当query_cache_type=2时,只有在查询语句中使用 SQL_CACHE 关键字,查询才会使用查询缓存区。使用查询缓存区可以提高查询的速度,这种方式只适用于修改操作少且经常执行相同的查询操作的情况。

sort_buffer_size :表示每个 需要进行排序的线程分配的缓冲区的大小 。增加这个参数的值可以提高 ORDER BY 或 GROUP BY 操作的速度。默认数值是2 097 144字节(约2MB)。对于内存在4GB左右的服务器推荐设置为6-8M,如果有100个连接,那么实际分配的总共排序缓冲区大小为100 × 6 = 600MB。

join_buffer_size = 8M :表示 联合查询操作所能使用的缓冲区大小 ,和sort_buffer_size一样,该参数对应的分配内存也是每个连接独享。

read_buffer_size :表示 每个线程连续扫描时为扫描的每个表分配的缓冲区的大小(字节) 。当线程从表中连续读取记录时需要用到这个缓冲区。SET SESSION read_buffer_size=n可以临时设置该参数的值。默认为64K,可以设置为4M。

innodb_flush_log_at_trx_commit :表示 何时将缓冲区的数据写入日志文件 ,并且将日志文件写入磁盘中。该参数对于innoDB引擎非常重要。该参数有3个值,分别为0、1和2。该参数的默认值为1。值为 0 时,表示 每秒1次 的频率将数据写入日志文件并将日志文件写入磁盘。每个事务的commit并不会触发前面的任何操作。该模式速度最快,但不太安全,mysqld进程的崩溃会导致上一秒钟所有事务数据的丢失。值为 1 时,表示 每次提交事务时 将数据写入日志文件并将日志文件写入磁盘进行同步。该模式是最安全的,但也是最慢的一种方式。因为每次事务提交或事务外的指令都需要把日志写入(flush)硬盘。值为 2 时,表示 每次提交事务时 将数据写入日志文件, 每隔1秒 将日志文件写入磁盘。该模式速度较快,也比0安全,只有在操作系统崩溃或者系统断电的情况下,上一秒钟所有事务数据才可能丢失。

innodb_log_buffer_size :这是 InnoDB 存储引擎的 事务日志所使用的缓冲区 。为了提高性能,也是先将信息写入 Innodb Log Buffer 中,当满足 innodb_flush_log_trx_commit 参数所设置的相应条件(或者日志缓冲区写满)之后,才会将日志写到文件(或者同步到磁盘)中。

max_connections :表示 允许连接到MySQL数据库的最大数量 ,默认值是 151 。如果状态变量
connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这是可以考虑增大max_connections 的值。在Linux 平台下,性能好的服务器,支持 500-1000 个连接不是难事,需要根据服务器性能进行评估设定。这个连接数 不是越大 越好 ,因为这些连接会浪费内存的资源。过多的连接可能会导致MySQL服务器僵死。

back_log :用于 控制MySQL监听TCP端口时设置的积压请求栈大小 。如果MySql的连接数达到
max_connections时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即
back_log,如果等待连接的数量超过back_log,将不被授予连接资源,将会报错。5.6.6 版本之前默
认值为 50 , 之后的版本默认为 50 + (max_connections / 5), 对于Linux系统推荐设置为小于512
的整数,但最大不超过900。如果需要数据库在较短的时间内处理大量连接请求, 可以考虑适当增大back_log 的值。

thread_cache_size : 线程池缓存线程数量的大小 ,当客户端断开连接后将当前线程缓存起来,当在接到新的连接请求时快速响应无需创建新的线程 。这尤其对那些使用短连接的应用程序来说可以极大的提高创建连接的效率。那么为了提高性能可以增大该参数的值。默认为60,可以设置为120。

当 Threads_cached 越来越少,但 Threads_connected 始终不降,且 Threads_created 持续升高,可
适当增加 thread_cache_size 的大小。

wait_timeout :指定 一个请求的最大连接时间 ,对于4GB左右内存的服务器可以设置为5-10。
interactive_timeout :表示服务器在关闭连接前等待行动的秒数。
这里给出一份my.cnf的参考配置:

[mysqld]
port = 3306
serverid = 1
socket = /tmp/mysql.sock
skip-locking
#避免MySQL的外部锁定,减少出错几率增强稳定性。
skip-name-resolve
#禁止MySQL对外部连接进行DNS解析,使用这一选项可以消除MySQL进行DNS解析的时间。但需要注意,如果开启该选项,则所有远程主机连接授权都要使用IP地址方式,否则MySQL将无法正常处理连接请求!
back_log = 384
key_buffer_size = 256M
max_allowed_packet = 4M
thread_stack = 256K
table_cache = 128K
sort_buffer_size = 6M
read_buffer_size = 4M
read_rnd_buffer_size=16M
join_buffer_size = 8M
myisam_sort_buffer_size = 64M
table_cache = 512 thread_cache_size = 64
query_cache_size = 64M
tmp_table_size = 256M
max_connections = 768
max_connect_errors = 10000000
wait_timeout = 10
thread_concurrency = 8
#该参数取值为服务器逻辑CPU数量2,在本例中,服务器有2颗物理CPU,而每颗物理CPU又支持H.T超线程,所以实际取值为42=8
skip- networking
#开启该选项可以彻底关闭MySQL的TCP/IP连接方式,如果WEB服务器是以远程连接的方式访问MySQL数据库服务器则不要开启该选项!否则将无法正常连接!
table_cache=1024
innodb_additional_mem_pool_size=4M
#默认为2M
innodb_flush_log_at_trx_commit=1
innodb_log_buffer_size=2M
#默认为1M
innodb_thread_concurrency=8
#你的服务器CPU有几个就设置为几。建议用默认一般为8
tmp_table_size=64M
#默认为16M,调到64-256最挂
thread_cache_size=120
query_cache_size=32M

很多情况还需要具体情况具体分析!

举例:

下面是一个电商平台,类似京东或天猫这样的平台。商家购买服务,入住平台,开通之后,商家可以在系统中上架各种商品,客户通过手机App、微信小程序等渠道购买商品,商家接到订单以后安排快递送货。

刚刚上线的时候,系统运行状态良好。但是,随着入住的商家不断增多,使用系统的用户量越来越多,每天的订单数据达到了5万条以上。这个时候,系统开始出现问题,CPU使用率不断飙升。终于,双十一或者618活动高峰的时候,CPU使用率达到99%,这实际上就意味着,系统的计算资源已经耗尽,再也无法处理任何新的订单了。换句话说,系统已经崩溃了。

这个时候,我们想到了对系统参数进行调整,因为参数的值决定了资源配置的方式和投放的程度。为了解决这个问题,一共调整3个系统参数,分别是

lnnoDB_flush_log_at_trx_commit
lnnoDB_buffer_pool_size
InnoDB_buffer_pool_instances

下面我们就说一说调整这三个参数的原因是什么。

(1)调整系统参数InnoDB_flush_log_at_trx_commit

这个参数适用于InnoDB存储引擎,电商平台系统中的表用的存储引擎都是InnoDB。默认的值是1,意思是每次提交事务的时候,都把数据写入日志,并把日志写入磁盘。这样做的好处是数据安全性最佳,不足之处在于每次提交事务,都要进行磁盘写入的操作。在大并发的场景下,过于频繁的磁盘读写会导致CPU资源浪费,系统效率变低。

这个参数的值还有2个可能的选项,分别是0和2。我们把这个参数的值改成了2。这样就不用每次提交事务的时候都启动磁盘读写了,在大并发的场景下,可以改善系统效率,降低CPU使用率。即便出现故障,损失的数据也比较小。

(2)调整系统参数InnoDB_buffer_pool_size

这个参数的意思是,InnoDB存储引擎使用缓存来存储索引和数据。这个值越大,可以加载到缓存区的索引和数据量就越多,需要的磁盘读写就越少。

因为我们的MySQL服务器是数据库专属服务器,只用来运行MySQL数据库服务,没有其他应用了,而我们的计算机是64位机器,内存也有128G。于是我们把这个参数的值调整为64G。这样一来,磁盘读写次数可以大幅降低,我们就可以充分利用内存,释放出一些CPU的资源。

(3)调整系统参数InnoDB_buffer_pool_instances

这个参数可以将InnoDB的缓存区分成几个部分,这样可以提高系统的并行处理能力,因为可以允许多个进程同时处理不同部分的缓存区。

我们把InnoDB_buffer_pool_instances的值修改为64,,意思就是把 InnoDB的缓存区分成64个分区,这样就可以同时有多个进程进行数据操作,CPU的效率就高多了。修改好了系统参数的值,要重启MySQL数据库服务器。

总结一下就是遇到CPU资源不足的问题,可以从下面2个思路去解决。

疏通拥堵路段,消除瓶颈,让等待的时间更短;
开拓新的通道,增加并行处理能力。

优化数据库结构

一个好的数据库设计方案对于数据库的性能常常会起到事半功倍的效果。合理的数据库结构不仅可以使数据库占用更小的磁盘空间,而且能够使查询速度更快。数据库结构的设计需要考虑数据冗余、查询和更新的速度、字段的数据类型是否合理等多方面的内容。

拆分表:冷热数据分离

拆分表的思路是,把1个包含很多字段的表拆分成2个或者多个相对较小的表。这样做的原因是,这些表中某些字段的操作频率很高(热数据),经常要进行查询或者更新操作,而另外一些字段的使用频率却很低(冷数据),冷热数据分离,可以减小表的宽度。如果放在一个表里面,每次查询都要读取大记录,会消耗较多的资源。

MySQL限制每个表最多存储4096列,并且每一行数据的大小不能超过65535字节。表越宽,把表装载进内存缓冲池时所占用的内存也就越大,也会消耗更多的lO。冷热数据分离的目的是:①减少磁盘IO,保证热数据的内存缓存命中率。②更有效的利用缓存,避免读入无用的冷数据。

举例1: 会员members表 存储会员登录认证信息,该表中有很多字段,如id、姓名、密码、地址、电话、个人描述字段。其中地址、电话、个人描述等字段并不常用,可以将这些不常用的字段分解出另一个表。将这个表取名叫members_detail,表中有member_id、address、telephone、description等字段。
这样就把会员表分成了两个表,分别为 members表 和 members_detail表 。

创建这两个表的SQL语句如下:

CREATE TABLE members ( 
	id int(11) NOT NULL AUTO_INCREMENT, 
	username varchar(50) DEFAULT NULL, 
	password varchar(50) DEFAULT NULL, 
	last_login_time datetime DEFAULT NULL, 
	last_login_ip varchar(100) DEFAULT NULL, 
	PRIMARY KEY(Id) 
);


CREATE TABLE members_detail ( 
	Member_id int(11) NOT NULL DEFAULT 0, 
	address varchar(255) DEFAULT NULL, 
	telephone varchar(255) DEFAULT NULL, 
	description text 
);

如果需要查询会员的基本信息或详细信息,那么可以用会员的id来查询。如果需要将会员的基本信息和
详细信息同时显示,那么可以将members表和members_detail表进行联合查询,查询语句如下:

SELECT * FROM members LEFT JOIN members_detail on members.id = members_detail.member_id;

通过这种分解可以提高表的查询效率。对于字段很多且有些字段使用不频繁的表,可以通过这种分解的方式来优化数据库的性能。

增加中间表

对于需要经常联合查询的表,可以建立中间表以提高查询效率。通过建立中间表,把需要经常联合查询的数据插入中间表中,然后将原来的联合查询改为对中间表的查询,以此来提高查询效率。

首先,分析经常联合查询表中的字段;然后,使用这些字段建立一个中间表,并将原来联合查询的表的数据插入中间表中;最后,使用中间表来进行查询。

举例1: 学生信息表 和 班级表 的SQL语句如下:

CREATE TABLE `class` ( 
	`id` INT(11) NOT NULL AUTO_INCREMENT, 
	`className` VARCHAR(30) DEFAULT NULL, 
	`address` VARCHAR(40) DEFAULT NULL, 
	`monitor` INT NULL , 
	PRIMARY KEY (`id`) 
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 

CREATE TABLE `student` ( 
	`id` INT(11) NOT NULL AUTO_INCREMENT, 
	`stuno` INT NOT NULL ,
	`name` VARCHAR(20) DEFAULT NULL, 
	`age` INT(3) DEFAULT NULL, 
	`classId` INT(11) DEFAULT NULL, 
	PRIMARY KEY (`id`) 
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

现在有一个模块需要经常查询带有学生名称(name)、学生所在班级名称(className)、学生班级班
长(monitor)的学生信息。根据这种情况可以创建一个 temp_student 表。temp_student表中存储学生
名称(stu_name)、学生所在班级名称(className)和学生班级班长(monitor)信息。创建表的语句
如下:

CREATE TABLE `temp_student` ( 
	`id` INT(11) NOT NULL AUTO_INCREMENT, 
	`stu_name` INT NOT NULL , 
	`className` VARCHAR(20) DEFAULT NULL, 
	`monitor` INT(3) DEFAULT NULL, 
	PRIMARY KEY (`id`) 
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

接下来,从学生信息表和班级表中查询相关信息存储到临时表中:

insert into temp_student(stu_name,className,monitor) 
	select s.name,c.className,c.monitor
	from student as s,class as c 
	where s.classId = c.id

以后,可以直接从temp_student表中查询学生名称、班级名称和班级班长,而不用每次都进行联合查
询。这样可以提高数据库的查询速度。

增加冗余字段

设计数据库表时应尽量遵循范式理论的规约,尽可能减少冗余字段,让数据库设计看起来精致、优雅。
但是,合理地加入冗余字段可以提高查询速度。

表的规范化程度越高,表与表之间的关系就越多,需要连接查询的情况也就越多。尤其在数据量大,而且需要频繁进行连接的时候,为了提升效率,我们也可以考虑增加冗余字段来减少连接。
这部分内容在《第11章_数据库的设计规范》章节中 反范式化小节 中具体展开讲解了。这里省略。

优化数据类型

改进表的设计时,可以考虑优化字段的数据类型。这个问题在大家刚从事开发时基本不算是问题。但是,随着你的经验越来越丰富,参与的项目越来越大,数据量也越来越多的时候,你就不能只从系统稳定性的角度来思考问题了,还要考虑到系统整体的稳定性和效率。此时,优先选择符合存储需要的最小的数据类型。

列的字段越大,建立索引时所需要的空间也就越大,这样一页中所能存储的索引节点的数量也就越少,在遍历时所需要的IO次数也就越多,索引的性能也就越差。

具体来说:

情况1:对整数类型数据进行优化。
遇到整数类型的字段可以用 INT 型 。这样做的理由是,INT 型数据有足够大的取值范围,不用担心数据超出取值范围的问题。刚开始做项目的时候,首先要保证系统的稳定性,这样设计字段类型是可以的。但在数据量很大的时候,数据类型的定义,在很大程度上会影响到系统整体的执行效率。

对于 非负型 的数据(如自增ID、整型IP)来说,要优先使用无符号整型 UNSIGNED 来存储。因为无符号相对于有符号,同样的字节数,存储的数值范围更大。如tinyint有符号为-128-127,无符号为0-255,多出一倍的存储空间。

情况2:既可以使用文本类型也可以使用整数类型的字段,要选择使用整数类型。
跟文本类型数据相比,大整数往往占用 更少的存储空间 ,因此,在存取和比对的时候,可以占用更少的内存空间。所以,在二者皆可用的情况下,尽量使用整数类型,这样可以提高查询的效率。如:将IP地址转换成整型数据。

情况3:避免使用TEXT、BLOB数据类型
MySQL内存临时表不支持TEXT、BLOB这样的大数据类型,如果查询中包含这样的数据,在排序等操作时,就不能使用内存临时表,必须使用磁盘临时表进行。并且对于这种数据,MySQL还是要进行二次查询,会使SQL性能变得很差,但是不是说一定不能使用这样的数据类型。

如果一定要使用,建议把BLOB或是TEXT列分离到单独的扩展表中,查询时一定不要使用select *,而只需要取出必要的列,不需要TEXT列的数据时不要对该列进行查询。

情况4:避免使用ENUM类型
修改ENUM值需要使用ALTER语句。
ENUM类型的ORDER BY操作效率低,需要额外操作。使用TINYINT来代替ENUM类型。

情况5:使用TIMESTAMP存储时间
TIMESTAMP存储的时间范围1970-01-01 00:00:01~2038-01-19-03:14:07。TIMESTAMP使用4字节,DATETIME使用8个字节,同时TIMESTAMP具有自动赋值以及自动更新的特性。

情况6:用DECIMAL代替FLOAT和DOUBLE存储精确浮点数
1)非精准浮点: float,double

2)精准浮点: decimal

Decimal类型为精准浮点数,在计算时不会丢失精度,尤其是财务相关的金融类数据。占用空间由定义的宽度决定,每4个字节可以存储9位数字,并且小数点要占用一个字节。可用于存储比bigint更大的整型数据。

总之,遇到数据量大的项目时,一定要在充分了解业务需求的前提下,合理优化数据类型,这样才能充分发挥资源的效率,使系统达到最优。

优化插入记录的速度

插入记录时,影响插入速度的主要是索引、唯一性校验、一次插入记录条数等。根据这些情况可以分别进行优化。这里我们分为MylSAM引擎和InnoDB存储引擎来讲。

  1. MyISAM引擎的表:
    ① 禁用索引

对于非空表,插入记录时,MySQL会根据表的索引对插入的记录建立索引。如果插入大量数据,建立索引就会降低插入记录的速度。为了解决这种情况,可以在插入记录之前禁用索引,数据插入完毕后再开启索引。禁用索引的语句如下:

ALTER TABLE table_name DISABLE KEYS;

重新开启索引的语句如下:

ALTER TABLE table_name ENABLE KEYS;

若对于空表批量导入数据,则不需要进行此操作,因为MylSAM引擎的表是在导入数据之后才建立索引的。

② 禁用唯一性检查

插入数据时,MySQL会对插入的记录进行唯一性校验。这种唯一性校验会降低插入记录的速度。为了降低这种情况对查询速度的影响,可以在插入记录之前禁用唯一性检查,等到记录插入完毕后再开启。

禁用唯一性检查的语句如下:

SET UNIQUE_CHECKS=0;

开启唯一性检查的语句如下:

SET UNIQUE_CHECKS=1;

③ 使用批量插入

插入多条记录时,可以使用一条INSERT语句插入一条记录,也可以使用一条INSERT语句插入多条记录。插入一条记录的INSERT语句情形如下:

insert into student values(1,'zhangsan',18,1); 
insert into student values(2,'lisi',17,1); 
insert into student values(3,'wangwu',17,1); 
insert into student values(4,'zhaoliu',19,1);

使用一条INSERT语句插入多条记录的情形如下:

insert into student 
values 
(1,'zhangsan',18,1), 
(2,'lisi',17,1), 
(3,'wangwu',17,1), 
(4,'zhaoliu',19,1);

第2种情形的插入速度要比第1种情形快。

④ 使用LOAD DATA INFILE 批量导入

当需要批量导入数据时,如果能用LOAD DATAINFILE语句,就尽量使用。因为LOAD DATA INFILE语句导入数据的速度比INSERT语句快。

  1. InnoDB引擎的表:
    ① 禁用唯一性检查

插入数据之前执行set unique_checks=0来禁止对唯一索引的检查,数据导入完成之后再运行setunique_checks=1。这个和MyISAM引擎的使用方法一样。

② 禁用外键检查

插入数据之前执行禁止对外键的检查,数据插入完成之后再恢复对外键的检查。禁用外键检查的语句如下:

SET foreign_key_checks=0;

恢复对外键的检查语句如下:

SET foreign_key_checks=1;

③ 禁止自动提交

插入数据之前禁止事务的自动提交,数据导入完成之后,执行恢复自动提交操作。禁止自动提交的语句如下:

set autocommit=0;

恢复自动提交的语句如下:

set autocommit=1 ;

使用非空约束

在设计字段的时候,如果业务允许,建议尽量使用非空约束,这样的好处是:

进行比较和计算时,省去要对NULL值的字段判断是否为空的开销,提高存储效率。
非空字段也容易创建索引。因为索引NULL列需要额外的空间来保存,所以要占用更多的空间。使用非空约束,就可以节省存储空间(每个字段1个bit)。

分析表、检查表与优化表

MysQL提供了分析表、检查表和优化表的语句。分析表主要是分析关键字的分布,检查表主要是检查表是否存在错误,优化表主要是消除删除或者更新造成的空间浪费。

分析表
MySQL中提供了ANALYZE TABLE语句分析表,ANALYZE TABLE语句的基本语法如下:

ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name[,tbl_name]

默认的,MySQL服务会将 ANALYZE TABLE语句写到binlog中,以便在主从架构中,从服务能够同步数据。
可以添加参数LOCAL 或者 NO_WRITE_TO_BINLOG取消将语句写到binlog中。

使用 ANALYZE TABLE 分析表的过程中,数据库系统会自动对表加一个 只读锁 。在分析期间,只能读取
表中的记录,不能更新和插入记录。ANALYZE TABLE语句能够分析InnoDB和MyISAM类型的表,但是不能
作用于视图。

ANALYZE TABLE分析后的统计结果会反应到 cardinality 的值,该值统计了表中某一键所在的列不重复
的值的个数。该值越接近表中的总行数,则在表连接查询或者索引查询时,就越优先被优化器选择使
用。也就是索引列的cardinality的值与表中数据的总条数差距越大,即使查询的时候使用了该索引作为查
询条件,存储引擎实际查询的时候使用的概率就越小。下面通过例子来验证下。cardinality可以通过
SHOW INDEX FROM 表名查看。

检查表
MySQL中可以使用 CHECK TABLE 语句来检查表。CHECK TABLE语句能够检查InnoDB和MyISAM类型的表
是否存在错误。CHECK TABLE语句在执行过程中也会给表加上 只读锁 。

对于MyISAM类型的表,CHECK TABLE语句还会更新关键字统计数据。而且,CHECK TABLE也可以检查视
图是否有错误,比如在视图定义中被引用的表已不存在。该语句的基本语法如下:

CHECK TABLE tbl_name [, tbl_name] ... [option] ... 
option = {QUICK | FAST | MEDIUM | EXTENDED | CHANGED}

其中,tbl_name是表名;option参数有5个取值,分别是QUICK、FAST、MEDIUM、EXTENDED和 CHANGED。各个选项的意义分别是:

QUICK :不扫描行,不检查错误的连接。

FAST :只检查没有被正确关闭的表。

CHANGED :只检查上次检查后被更改的表和没有被正确关闭的表。

MEDIUM :扫描行,以验证被删除的连接是有效的。也可以计算各行的关键字校验和,并使用计算
出的校验和验证这一点。

EXTENDED :对每行的所有关键字进行一个全面的关键字查找。这可以确保表是100%一致的,但
是花的时间较长。

option只对MyISAM类型的表有效,对InnoDB类型的表无效。比如:

image.png

该语句对于检查的表可能会产生多行信息。最后一行有一个状态的 Msg_type 值,Msg_text 通常为 OK。
如果得到的不是 OK,通常要对其进行修复;是 OK 说明表已经是最新的了。表已经是最新的,意味着存
储引擎对这张表不必进行检查。

优化表

方式1:OPTIMIZE TABLE

MySQL中使用 OPTIMIZE TABLE 语句来优化表。但是,OPTILMIZE TABLE语句只能优化表中的VARCHAR 、 BLOB 或 TEXT 类型的字段。一个表使用了这些字段的数据类型,若已经 删除 了表的一大部分数据,或者已经对含有可变长度行的表(含有VARCHAR、BLOB或TEXT列的表)进行了很多 更新 ,则应使用OPTIMIZE TABLE来重新利用未使用的空间,并整理数据文件的 碎片 。 OPTIMIZE TABLE 语句对InnoDB和MyISAM类型的表都有效。该语句在执行过程中也会给表加上 只读锁 。 OPTILMIZE TABLE语句的基本语法如下:

OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] 

LOCAL | NO_WRITE_TO_BINLOG关键字的意义和分析表相同,都是指定不写入二进制日志。

image.png

执行完毕,Msg_text显示

‘numysql.SYS_APP_USER’, ‘optimize’, ‘note’, ‘Table does not support optimize, doing recreate + analyze instead’

原因是我服务器上的MySQL是InnoDB存储引擎。

到底优化了没有呢?看官网!

https://dev.mysql.com/doc/refman/8.0/en/optimize-table.html

在MyISAM中,是先分析这张表,然后会整理相关的MySQL datafile,之后回收未使用的空间;在InnoDB
中,回收空间是简单通过Alter table进行整理空间。在优化期间,MySQL会创建一个临时表,优化完成之
后会删除原始表,然后会将临时表rename成为原始表。

说明: 在多数的设置中,根本不需要运行OPTIMIZE TABLE。即使对可变长度的行进行了大量的更
新,也不需要经常运行, 每周一次 或 每月一次 即可,并且只需要对 特定的表 运行。

小结

上述这些方法都是有利有弊的。比如:

修改数据类型,节省存储空间的同时,你要考虑到数据不能超过取值范围;
增加冗余字段的时候,不要忘了确保数据一致性;

把大表拆分,也意味着你的查询会增加新的连接,从而增加额外的开销和运维的成本。
因此,你一定要结合实际的业务需求进行权衡。

大表优化

当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:

限定查询的范围

禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内;

读/写分离

经典的数据库拆分方案,主库负责写,从库负责读。

一主一从模式

双主双从模式:

垂直拆分

当数据量级达到 千万级 以上时,有时候我们需要把一个数据库切成多份,放到不同的数据库服务器上,
减少对单一数据库服务器的访问压力。

如果数据库中的数据表过多,可以采用垂直分库的方式,将关联的数据表部署在同一个数据库上。
如果数据表中的列过多,可以采用垂直分表的方式,将一张数据表分拆成多张数据表,把经常一起使用的列放到同一张表里。

垂直拆分的优点: 可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区
可以简化表的结构,易于维护。

垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起 JOIN 操作。此外,垂直拆分会让事务变得更加复杂。

水平拆分

水平拆分能够支持非常大的数据量存储,应用端改造也少,但分片事务难以解决,跨节点Join性能较差,逻辑复杂。《Java工程师修炼之道》的作者推荐·尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。

下面补充一下数据库分片的两种常见方案:

客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的
Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。我们现在
谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。

其它调优策略

服务器语句超时处理

在MySQL 8.0中可以设置 服务器语句超时的限制 ,单位可以达到 毫秒级别 。当中断的执行语句超过设置的
毫秒数后,服务器将终止查询影响不大的事务或连接,然后将错误报给客户端。

设置服务器语句超时的限制,可以通过设置系统变量 MAX_EXECUTION_TIME 来实现。默认情况下,MAX_EXECUTION_TIME的值为0,代表没有时间限制。 例如:

SET SESSION MAX_EXECUTION_TIME=2000; #指定该会话中SELECT语句的超时时间

创建全局通用表空间

MySQL 8.0使用 CREATE TABLESPACE语句来创建一个全局通用表空间。全局表空间可以被所有的数据库的表共享,而且相比于独享表空间,使用手动创建共享表空间可以节约元数据方面的内存。可以在创建表的时候,指定属于哪个表空间,也可以对已有表进行表空间修改等。

下面创建名为atguigu1的共享表空间,SQL语句如下:

CREATE TABLESPACE atguigu1 ADD datafile 'atguigu1.ibd' file_block_size=16k;

指定表空间,SQL语句如下:

CREATE TABLE test(id int, name varchar(10)
) engine=innodb default charset utf8mb4 tablespace atguigu1 ;

MySQL 8.0新特性:隐藏索引对调优的帮助

不可见索引的特性对于性能调试非常有用。在MySQL 8.0中,索引可以被“隐藏"和“显示"。当一个索引被隐藏时,它不会被查询优化器所使用。也就是说,管理员可以隐藏一个索引,然后观察对数据库的影响。如果数据库性能有所下降,就说明这个索引是有用的,于是将其"恢复显示"即可;如果数据库性能看不出变化,就说明这个索引是多余的,可以删掉了。

需要注意的是当索引被隐藏时,它的内容仍然是和正常索引一样实时更新的。如果一个索引需要长期被隐藏
那么可以将其删除,因为索引的存在会影响插入、更新和删除的性能。

数据表中的主键不能被设置为invisible。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值