文章目录
- MySQL查询原理与优化
- 一、select语句的执行顺序
- 二、join 的执行与优化
- 1、驱动表 & 被驱动表
- 2、Simple Nested Loop Join
- 3、Index Nested Loop Join
- 4、Block Nested Loop Join
- 5、Hash Join
- 6、join 优化小结
- 三、on 与 where 对比
- 四、group by 的执行与优化
- 1、group by 执行
- 2、Explain - Using temporary
- 3、group by 优化小结
- 五、order by 的执行与优化
- 1、FireSort 排序算法(两种)
- 1)单路排序(快)
- 2)双路排序(慢)
- 3)单路排序 vs 双路排序
- 4)选择算法的依据
- 2、FireSort 相关参数
- 1)sort_buffer
- 2)max_length_for_sort_data
- 3、使用索引优化排序
- 1)联合索引
- 2)覆盖索引(减少回表)
- 4、关联查询 与 排序
- 5、IndexSort 失效情况
- 1)最左匹配原则
- 2)排序字段顺序与索引不一致
- 3)排序规则不一致
- 4)回表数据太多
- 5)小结
- 6、Explain - Using filesort
- 7、FileSort排序效率一定差?
- 8、order by 优化小结
- 9、order by 注意事项
- 六、limit 的执行与优化
- 1、子查询关联
- 2、自增id - maxId
- 七、子查询 的执行与优化
- 1、in子查询分析
- 2、semi join
- 3、materialization
- 4、子查询的优化器策略
- 5、in 和 exists
- 6、子查询 优化小结
- 八、其他优化
- 1、几种count性能对比
- 2、别用 select *
- 3、使用 limit1
- 4、及时commit
MySQL查询原理与优化
一、select语句的执行顺序
select distinct 字段/聚合函数
from 表1 别名
inner join 表2 别名
on 连接条件
where 筛选(聚合之前)
group by 分组列表 -- 这里开始可以使用 select 中的别名
having 筛选(聚合之后)
order by 排序列表
limit 起始条目索引,条目数;
1. from
对from子句中的前两个表计算笛卡尔积,产生虚拟表VT1。(选择相对小的表做基础表)
2. on
对虚拟表VT1进行on条件过滤,筛选出满足 <on-condition> 的行,产生虚拟表VT2。
3. join
如果指定了外连接,表中未匹配的行就会作为外部行添加到虚拟表VT2中,产生虚拟表VT3。
left join :将左表在第二步中过滤掉的行添加进来。
right join :将右表在第二步中过滤掉的行添加进来。
4. where
对虚拟表VT3进行where条件过滤。符合 <where-condition> 的记录插入到虚拟表VT4中。
5. group by
根据 group by 子句中的列,对虚拟表VT4中的记录进行分组操作,产生虚拟表VT5。
6. having
对虚拟表VT5进行having过滤,符合 <having-condition> 的记录插入到虚拟表VT6中。
7. select
筛选出虚拟表VT6中select指定的列,插入到虚拟表VT7中。
8. distinct
移除虚拟表VT7中相同的行,产生虚拟表VT8。
9. order by
将虚拟表VT8中的记录按照 <order by list> 进行排序操作,返回游标。
10. limit
取出指定行的记录,产生虚拟表VT9, 并作为结果集返回。
- 聚合函数的执行顺序:
聚合函数
是在group by之后
,having之前
执行的。
- 别名的使用:
- 在oracle中,别名的使用都是严格遵循sql执行顺序的,group by后面不能用别名。
- 在mysql中,group by 开始就可以使用别名。
- 其他注意点:
having
筛选器 是第一个
也是唯一一个
应用到已分组数据
的筛选器。order by
返回的不是虚拟表,而是游标
(包含特定的物理顺序的逻辑组织的对象)
二、join 的执行与优化
1、驱动表 & 被驱动表
对于外连接,以左外连接
a left join b
为例
- 以a表作为
驱动表
,b表作为被驱动表
(也可能会被优化为内连接,那就不一定是a为驱动表了) - 如果只能添加一个字段的索引,一定要给
被驱动表
添加索引 - 被驱动表如果没有索引,会有一个
Using join buffer
的优化- 8.0.18版本之前默认:
Using join buffer (Block Nested Loop)
- 8.0.18版本开始默认:
Using join buffer (hash join)
- 8.0.18版本之前默认:
对于内连接,例如
a inner join b
- 查询优化器是可以决定谁作为驱动表,谁作为被驱动表的
- 如果两张表都有索引 / 两张表都没有索引,则小表作为
驱动表
,大表作为被驱动表
(小表驱动大表) - 如果一张表有索引,一张表没有索引,则没有索引的表作为
驱动表
,有索引的表作为被驱动表
(有索引的被驱动)
- 如果两张表都有索引 / 两张表都没有索引,则小表作为
Tips:Explain的结果中,上边一行为驱动表,下边一行为被驱动表
2、Simple Nested Loop Join
从表A取出一条数据,遍历表B,将匹配到的数据放到result(驱动表A中的每一条记录都与被驱动表B的所有记录进行匹配)
这种方式的效率是非常低的,假设A表数据100,B表数据1000条,则 join比较次数 为 A*B=10w
次
3、Index Nested Loop Join
lndex Nested-Loop Join 优化的思路主要是为了减少内层表数据的匹配次数,所以要求被驱动表上必须有索引才行。
从表A取出一条数据,通过索引匹配表B中的数据(避免和内层表的每条记录进行比较,极大的减少了和内层表的匹配次数)
驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本比较固定,故mysql优化器都倾向使用记录数少的作为驱动表
如果被驱动表加的索引不是主键索引,还得进行一次回表查询。因此,如果被驱动表的索引是主键索引,效率会更高。
4、Block Nested Loop Join
如果存在索引,那么会使用index的方式进行join,如果join的列没有索引,则每次都要从驱动表中加载一条记录,然后把被驱动表的记录加载到内存与其匹配,匹配结束后清除内存,继续匹配。这样周而复始,大大增加了IO的次数。
Block Nested Loop Join 优化的思路是减少IO的次数,引入 join buffer缓冲区,不再逐条获取驱动表的数据,而是一块一块的获取。
将驱动表join相关的部分数据(大小受join buffer的限制)缓存到 join buffer中,然后全表扫描被驱动表,一次性和 join buffer中的所有驱动表记录进行匹配(内存中操作),将 Simple Nested Loop Join 中的多次比较合并成一次,降低了被驱动表的访问频率。
注意:这里缓存的不只是关联表的列,select后面的列也会放到 join buffer 中,因此查询的时候尽量减少不必要的字段。
Block Nested Loop Join 相关信息的查看:
-
block_nested_loop
通过
show variables like %optimizer_switch%
查看 block_nested_loop 状态。(默认开启) -
join_buffer_size
驱动表批量获取的数量取决于 join buffer 的大小,可以通过
show variables like %join_buffer%
查看(默认256k)
5、Hash Join
从MySQL的8.0.20版本开始将废弃BNLJ,因为从MySQL8.0.18版本开始就加入了hash join,默认都会使用hash join
-
Nested Loop:
对于被连接的数据子集较小的情况下,Nested Loop是个较好的选择。
-
Hash Join
Hash Join是做大数据集连接时的常用方式。优化器使用两个表中相对较小的表,利用Join Key在内存中建立散列值,然后扫描相对
较大的表,并探测散列值,找出与Hash表匹配的行。
Hash Join 的特点:
- 这种方式适用于较小的表完全可以放入内存中的情况,这样总成本就是访问两个表的成本之和。
- 在表很大的情况下并不能完全放入内存,这时优化器会将它分割成若干不同的分区,不能放入内存的部分就把该分区写入磁盘的临时段,此时要求有较大的临时段从而尽量提高I/O的性能。
- Hash Join 能够很好的工作于没有索引的大表和并行查询的环境中,并提供最好的性能。
- Hash Join 只能应用于等值连接,这是由Hash的特点决定的。
6、join 优化小结
- 整体效率比较:
- Index NLJ > Block NLJ > Simple NLJ
小表驱动大表
(本质就是减少外层循环的数据数量)(小表的衡量标准是:表行数*每行大小)- 表行数:不是看总行数,还要考虑where条件过滤后的结果
- 行大小:select的列的数据总大小
- 一般来说只需要
为被驱动表的关联字段创建索引
(减少内层表的循环匹配次数) - 确保任何的 GROUP BY 和 ORDER BY 中的表达式只涉及到一个表中的列(这样MySQL才有可能使用索引来优化这个过程)
- 增大 join buffer size 的大小(一次缓存的数据越多,被驱动表的IO次数就越少)
- 减少 驱动表 不必要的字段查询(字段越少,join buffer 缓存的数据越多)
三、on 与 where 对比
两张表在 join 时,首先会计算笛卡尔积
- on后面的条件会对这个笛卡尔积做一个过滤,形成一张临时表
- 如果没有where,就直接返回结果;如果有where,就对上一步的临时表再进行过滤。
-- 使用inner join时(结果没有区别)
select * from A inner join B on condition1 and condition2
select * from A inner join B on condition1 where condition2
-- 使用left join时(condition1只对B生效,condition2对A和B都生效)
select * from A left join B on condition1 where condition2
-- 使用right join时(condition1只对A生效,condition2对A和B都生效)
select * from A right join B on condition1 where condition2
on的condition过滤的行还可以在第三步outer join时再次添加回来,而where的过滤就是最终的。
- 对于内连接来说,on和where没有区别
- 对于左连接来说,on后面的条件只对右表有效;
- 对于右连接来说,on后面的条件只对左表有效;
四、group by 的执行与优化
1、group by 执行
group by:
1、被分组字段有几类,最终结果集中就有几条(每一类只会显示首条结果)
-- 如sex有男,女两类,结果集就只有2条
2、分组查询中,select之后跟【与分组字段一对一关系的字段】和【聚合函数】
-- 因为只会取首条结果,如果不是一对一的关系,没有意义
3、一般和聚合函数一起使用:
-- 无分组,聚合函数将表中所有符合条件的数据当成一组。
-- 有分组,聚合在分组之后执行,对每一组数据进行聚合
4、分组之后就不需要再去重了:
-- 分组的时候是将列中唯一的值分成一组,这意味着所有的记录都是不同的。
2、Explain - Using temporary
使用Explain分析时,如果Extra出现Using temporary
:
- 说明mysql会创建一个内部的临时表来执行操作
这时候就要注意是否需要对group by进行优化了
3、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会很慢。
五、order by 的执行与优化
无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序
或者尽可能避免对大量数据进行排序
。
select city, name, age from t where city='杭州' order by name limit 1000;
1、FireSort 排序算法(两种)
当不能使用索引生成排序结果的时候,MySQL需要自己进行排序:
数据量小
:排序在内存中
进行数据量大
:需要使用磁盘
辅助排序。
MySQL将这个过程统一称为文件排序
(Using Filesort),即使完全是内存排序不需要任何磁盘文件时也是如此。
1)单路排序(快)
先读取查询所需要的所有列,然后再根据指定列进行排序,最后直接返回排序结果。(MySQL 4.1才引入)
单路排序的流程
- 初始化
sort_buffer
,确定放入的字段(要排序的字段 & 要查询的字段
) - 从
city索引树
找到所有符合where条件
的主键id
。 - 到
主键索引树
获取 name、city、age三个字段的值,存入sort_buffer中;(回表查询) - 对
sort_buffer
中的数据,按照排序字段name
做快速排序; - 按照排序结果,取结果集的 前1000行 返回给客户端。
2)双路排序(慢)
读取
行指针
和需要排序的字段
,对其进行排序,然后再根据排序结果读取所需要的数据行。
双路排序的流程
- 初始化
sort_buffer
,确定放入的字段(要排序的字段 & 主键id
) - 从
city索引树
找到所有符合where条件
的主键id
。 - 到
主键索引树
获取 name、id这两个字段的值,存入sort_buffer中;(回表查询1) - 对
sort_buffer
中的数据,按照排序字段name
做快速排序; - 按照排序结果,取结果集的 前1000行
- 根据
主键id
的值,到原表中取出city、name和age三个 要查询的字段 返回给客户端。(回表查询2)
3)单路排序 vs 双路排序
【单路排序】
- 从磁盘读取
要排序的字段
和要返回的字段
,放到sort_buffer中。(第1次回表)- 读取字段多,占用的 sort_buffer 更多,发生
外部排序
的概率更大。
- 读取字段多,占用的 sort_buffer 更多,发生
- 排序后直接从内存中返回
要查询的字段
。(不需要再回表查询)
【双路排序】
- 从磁盘读取
要排序的字段
和主键id
,放到sort_buffer中。(第1次回表)- 只需要读取少量字段,发生
外部排序
的概率更小。
- 只需要读取少量字段,发生
- 排序后根据
主键id
再回表查询要返回的字段
。(第2次回表)
4)选择算法的依据
MySQL选择算法的依据是内存是否足够
,即 单行的长度(要查询的所有列的总长度)是否超过 max_length_for_sort_data
- 内存足够:选择单路排序,这样排序后可以从内存中
直接返回
查询结果,不用再回到原表去取数据。 - 内存不够:改用双路排序,这样排序过程中
一次可以排序更多行
,但是需要再回到原表去取数据。
这也就体现了MySQL的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问。
2、FireSort 相关参数
1)sort_buffer
MySQL会给每个线程
分配一块内存用于排序,称为 sort_buffer
。
FireSort 可能在内存中完成,也可能需要使用外部排序,这取决于参数 sort_buffer_size
和 排序所需的内存
。
- 排序的数据量 小于 sort_buffer_size:排序就在内存中完成。(快速排序)
- 排序的数据量 大于 sort_buffer_size:内存放不下,则不得不利用
磁盘临时文件
辅助排序。(归并排序)- MySQL会先将数据分块,对每个独立的块使用 快速排序 进行排序,并将各个块的排序结果存放在磁盘上。
- 然后将各个排好序的块进行合并(merge),最后返回排序结果。
提高 sort_buffer_size
会降低外部排序的概率,减少磁盘临时文件的生成,减少磁盘I/O。
set sort_buffer_size = 524288;
注意事项:
MySQL在排序时,对每一个排序记录
都会分配一个足够大的定长空间
来存放(根据数据库定义的字段结构大小)
- 如果是 VARCHAR 列,则需要分配其完整长度,以容纳其中最长的字符串。
- 如果使用UTF-8字符集,那么MySQL将会为每个字符预留三个字节。
因此,排序消耗的临时空间
可能要比磁盘上的原表要大很多。
2)max_length_for_sort_data
如果 要查询的所有列的总长度
超过了 max_length_for_sort_data的大小
,MySQL就会使用双路排序
。
提高 max_length_for_sort_data
会降低改用双路排序
的概率。
set max_length_for_sort_data = 8192;
3、使用索引优化排序
MySQL之所以需要生成临时表,并且在临时表上做排序操作,是因为原来的数据都是无序的
。如果能够保证从索引树取出的数据,天然就是有序的话,就可以不用再排序了。
InnoDB的索引树 默认就是按照索引字段递增排序的
,因此只要对排序字段加上索引,就可以不用再排序了。
1)联合索引
# 创建一个city和name的联合索引
alter table t add index city_user(city, name);
这样就保证了,取“下一条记录”的遍历过程中,只要city的值是杭州,name的值就一定是有序的。
select city, name, age from t where city='杭州' order by name limit 1000;
索引查询的过程(有序)
- 从 索引 (city,name) 找到第一个满足
city='杭州'
条件的主键id; - 到
主键索引树
获取整行,取name、city、age三个字段的值,作为结果集的一部分直接返回; - 从索引 (city,name) 取下一个记录主键id;
- 重复步骤2、3,直到查到第1000条记录,或者是不满足
city='杭州'
条件时循环结束。
2)覆盖索引(减少回表)
# 添加覆盖索引进一步简化流程(无需回表查询)
alter table t add index city_user_age(city, name, age);
覆盖索引查询的过程(有序且无需回表)
-
从索引 (city,name,age) 找到第一个满足
city='杭州'
条件的记录,取出其中的name、city、age三个字段的值,作为结果集的一部分直接返回;
-
从索引 (city,name,age) 取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回;
-
重复执行步骤2,直到查到第1000条记录,或者是不满足
city='杭州'
条件时循环结束。
4、关联查询 与 排序
在关联查询的时候,如果需要排序,MySQL会分两种情况来处理这样的文件排序。
- 如果所有排序字段都来自关联的第一个表,那么MySQL在关联处理第一个表的时候就进行文件排序。
- 在MySQL的 EXPLAIN 结果中可以看到 Extra 字段会有
Using filesort
。
- 在MySQL的 EXPLAIN 结果中可以看到 Extra 字段会有
- 除此之外的所有情况,MySQL都会先将关联的结果存放到一个临时表中,然后在所有的关联都结束后,再进行文件排序。
- 在MySQL的 EXPLAIN 结果中可以看到 Extra 字段会有
Using temporary; Using filesort
。
- 在MySQL的 EXPLAIN 结果中可以看到 Extra 字段会有
如果查询中有 LIMIT 的话,LIMIT 也会在排序之后应用,所以即使需要返回较少的数据,临时表和需要排序的数据量仍然会非常大。
MySQL 5.6 在这里做了很多重要的改进。当只需要返回部分排序结果时,例如使用了LIMIT 子句,MySQL不再对所有的结果进行排序,而是根据实际情况,选择抛弃不满足条件的结果,然后再进行排序。
5、IndexSort 失效情况
满足以下条件,MySQL才能在排序的时候使用上索引(Index排序)。
- ORDER BY 子句 需要满足索引的最左匹配原则
- 索引的列顺序 和 ORDER BY 子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时。
- 回表数据不能太多,否则优化器认为回表代价太大,会直接选择FileSort
- 如果查询需要关联多张表,则只有当 ORDER BY 子句引用的字段全部为第一个表时,才能使用索引做排序。
MySQL可以使用同一个索引既满足排序,又用于查找行。设计索引时应该尽可能地同时满足这两种任务,这样是最好的。
这里测试使用的仍然是:MySQL索引 第五节的 class 和 student 表
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;
# 添加索引
CREATE INDEX idx_age_name_classid ON student(age, `name`, classid)
1)最左匹配原则
组合索引左边的列必须存在
# Using filesort(联合索引最左边的列age不存在)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY `name` limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY classid limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY `name`, classid limit 10;
# Using filesort(classid左边的列`name`不存在)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, classid limit 10;
# Using filesort(stuno不属于联合索引)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, `name`, classid, stuno limit 10;
# index排序(符合最左匹配原则)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, `name` limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, `name`, classid limit 10;
如果 WHERE 子句 或 JOIN 子句中 对 左边列 指定了 常量 的时候,ORDER BY可以缺少左边列
# index排序(where子句中用到了联合索引的最左字段)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE age=45 ORDER BY `name` limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE age=45 ORDER BY `name`, classid limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE age=45 and `name`='zs' ORDER BY classid limit 10;
# index排序(join子句中用到了联合索引的最左字段)
EXPLAIN SELECT SQL_NO_CACHE classid FROM student s INNER JOIN class c ON s.classid = c.id
WHERE age=45 ORDER BY `name`, classid;
EXPLAIN SELECT SQL_NO_CACHE classid FROM student s INNER JOIN class c
ON s.classid = c.id AND age=45 ORDER BY `name`, classid;
除了常量,使用范围查询或模糊查询都不行
# Using filesort(范围查询)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE age>45 ORDER BY `name` limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE age>45 ORDER BY `name`, classid limit 10;
# Using filesort(in)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE age IN (18,28) ORDER BY `name` limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE age IN (18,28) ORDER BY `name`, classid limit 10;
# Using filesort(模糊查询)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student
WHERE age=45 and `name` LIKE 'z%' ORDER BY classid limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student
WHERE age=45 and `name` LIKE '%z%' ORDER BY classid limit 10;
2)排序字段顺序与索引不一致
和 WHERE的组合索引规则 不同的是,ORDER BY的字段顺序 必须与 组合索引的字段顺序 完全一致,否则无法使用index排序
# where使用上了索引(和联合索引顺序不一致)
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE classid = 100 and name = 'Abel' and age = 10;
# Using filesort(和联合索引顺序不一致)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY classid, `name`, age limit 10;
# order by使用上了索引(和联合索引顺序完全一致)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, `name`, classid limit 10;
3)排序规则不一致
ORDER BY 子句排序规则不一致(同时存在 升序asc 和 降序desc)
# Using filesort(ORDER BY子句 同时存在 升序、降序)
EXPLAIN SELECT SQL_NO_CACHE classid FROM student WHERE age = 45 ORDER BY `name` DESC, classid ASC;
EXPLAIN SELECT SQL_NO_CACHE classid FROM student WHERE age = 45 ORDER BY `name` ASC, classid DESC;
# index排序
EXPLAIN SELECT SQL_NO_CACHE classid FROM student WHERE age = 45 ORDER BY `name` ASC, classid ASC;
EXPLAIN SELECT SQL_NO_CACHE classid FROM student WHERE age = 45 ORDER BY `name` DESC, classid DESC;
4)回表数据太多
查询的数据量很大,并且查询的字段需要回表,优化器会认为回表的代价太大,直接选择FileSort,而不使用index排序
# Using filesort(查询的字段都需要回表查询,而且数据量很大,优化器认为回表代价太大,直接选择FileSort)
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age, `name`;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, `name`, classid;
# index排序(查询字段使用了主键索引,不用回表)
EXPLAIN SELECT SQL_NO_CACHE id FROM student ORDER BY age, `name`, classid;
# index排序(查询字段使用了覆盖索引,不用回表)
EXPLAIN SELECT SQL_NO_CACHE age, `name`, classid FROM student ORDER BY age, `name`, classid;
如果 使用了limit后数据量很少,优化器认为回表代价比较小,会使用index排序
# index排序(limit后数据量少,回表代价小,优化器选择使用索引)
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age, `name`, classid limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, `name`, classid limit 10;
# Using filesort(limit后数据量仍然很大)
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age, `name`, classid limit 100000;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, `name`, classid limit 100000;
如果 使用了where筛选后数据量很少,优化器也认为回表代价比较小,会使用index排序
# index排序(where条件筛选过后数据量少)
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=45 ORDER BY `name`, classid limit 100000;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE stuno=1 ORDER BY age, `name`, classid limit 100000;
5)小结
# 建立组合索引(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 /* 对于排序来说,多个相等条件也是范围查询 */
6、Explain - Using filesort
使用Explain分析时,如果Extra出现Using filesort
:
- 说明排序没有使用到索引,只能在内存中(记录较少的时候)或磁盘中(记录较多的时候)进行文件排序
这时候就要注意是否需要对order by进行优化了
7、FileSort排序效率一定差?
有 Using filesort
的效率不一定就低,要具体问题具体分析
# 只有age使用了索引,没有Using filesort
CREATE INDEX idx_age_name ON student(age,name);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 45 AND stuno < 10000 ORDER BY name; /*0.121s*/
# age和stuno使用了索引,Using filesort排序
CREATE INDEX idx_age_stuno_name ON student(age,stuno,name);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 45 AND stuno < 10000 ORDER BY name; /*0.002s*/
原因:
- 所有的排序都是在条件过滤之后才执行的。所以,如果条件过滤掉大部分数据的话,剩下几百几千条数据进行排序其实并不是很消耗性能,即使索引优化了排序,但实际提升性能很有限。
- 相对的
stuno < 101000
这个条件,如果没有用到索引的话,要对几万条的数据进行扫描,这是非常消耗性能的,所以索引放在这个字段上性价比更高,是最优选择。
结论:
- 两个索引同时存在,mysql自动选择最优的方案(对于这个例子,mysql 选择 idx_age_stuno_name)。但是, 随着数据量的变化,选择的索引也会随之变化的 。
- 当【范围条件】和【group by 或者 order by】的字段出现二选一时,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在【范围条件】的字段上。反之,亦然。
8、order by 优化小结
-
尽量使用索引优化排序,避免FileSort排序(具体问题具体分析,参考第7点)
- 索引可以保证数据的有序性,不需要再到内存中进行排序,效率更高。
- FileSort排序一般在内存中进行,占用CPU较多,还有可能产生
磁盘临时文件
导致多次I/O,效率较低。
-
实在无法使用索引时,优化FileSort排序
-
提高 sort_buffer_size
排序的数据量 大于
sort_buffer_size
时,会利用磁盘临时文件
辅助排序,导致多次I/O,降低效率。 -
提高 max_length_for_sort_data
如果
要查询的所有列的总长度
超过了max_length_for_sort_data
时,就会使用双路排序
。 -
*order by 时不要使用 select ,只 select 需要的字段
减少每次写入 sort_buffer 的数据大小,降低 MySQL 改用
双路排序
的概率。
-
9、order by 注意事项
- 返回的不是虚拟表,而是游标(包含特定的物理顺序的逻辑组织的对象)
- sql是基于集合的理论的,集合不会预先对他的行排序,它只是成员的逻辑集合,成员的顺序是无关紧要的。
- 对表进行排序的查询可以返回一个对象,这个对象包含特定的物理顺序的逻辑组织。这个对象就叫游标。
- 正因为返回值是游标,所以使用 order by 子句查询不能应用于表达式。
- 排序是很需要成本的,除非你必须要排序,否则最好不要指定order by。
六、limit 的执行与优化
limit查询的offset越大,查询越慢
-- 时间: 0.001ms
select * from student limit 0,20
-- 时间: 0.006ms
select * from student limit 10000,20
-- 时间: 0.037ms
select * from student limit 100000,20
-- 时间: 0.108ms
select * from student limit 1000000,20
1、子查询关联
# 在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容
EXPLAIN SELECT * FROM student t, (SELECT id FROM student ORDER BY id LIMIT 300000,20) a WHERE t.id = a.id;
2、自增id - maxId
# 如果是自增ID,也可以用如下方式
SELECT max(id) as maxId FROM student LIMIT 300000;
EXPLAIN SELECT * FROM student WHERE id > maxId LIMIT 20;
七、子查询 的执行与优化
参考文章:https://blog.51cto.com/wujianwei/2534400?source=drt
1、in子查询分析
有以下子查询示例:
SELECT * FROM t1 WHERE t1.a IN (SELECT t2.b FROM t2 WHERE id < 10);
你肯定认为这个 SQL 会这样执行:
select t2.b from t2 where id < 10; -- 结果:1,2,3,4,5,6,7,8,9
select * from t1 where t1.a in(1,2,3,4,5,6,7,8,9);
而 MySQL5.6 以前,MySQL 会将相关的外层表压到子查询中,优化器认为这样效率更高。也就是说,是像下面这样执行的
select * from t1 where exists(select b from t2 where id < 10 and t1.a=t2.b);
执行计划为:
+----+--------------------+-------+-------+---------+------+----------+-------------+
| id | select_type | table | type | key | rows | filtered | Extra |
+----+--------------------+-------+-------+---------+------+----------+-------------+
| 1 | PRIMARY | t1 | ALL | NULL | 100 | 100.00 | Using where |
| 2 | DEPENDENT SUBQUERY | t2 | range | PRIMARY | 9 | 10.00 | Using where |
+----+--------------------+-------+-------+---------+------+----------+-------------+
不相关子查询 变成了 相关子查询(select_type:DEPENDENT SUBQUERY),子查询需要根据 b 来关联外表 t1,因为需要外表的 t1 字段,所以子查询是没法先执行的。执行流程为:
- 扫描 t1,从 t1 取出一行数据 R;
- 从数据行 R 中,取出字段 a 执行子查询,如果得到结果为 TRUE,则把这行数据 R 放到结果集;
- 重复 1、2 直到结束。
总的扫描行数为 100+100*9=1000(这是理论值,实际值为 964,看规律是子查询结果集每多一行,总扫描行数就会少几行)。
2、semi join
这样会有个问题,如果外层表是一个非常大的表,对于外层查询的每一行,子查询都得执行一次,这个查询的性能会非常差。我们很容易想到将其改写成 join 来提升效率:
select t1.* from t1 inner join t2 on t1.a=t2.b and t2.id<10;
这样优化可以让 t2 表做驱动表,t1表关联字段有索引,查找效率非常高。
但这里会有个问题,join 是有可能得到重复结果的,而 in (select …) 子查询语义则不会得到重复值。
在这种情况下,可以使用 semi join 来优化子查询。semi join 是 MySQL5.6 加入的新特性,是解决重复值问题的一种特殊联接。
MySQL5.6 以前,优化器只有 exists 一种策略来优化子查询。
经过 semijoin 优化后的SQL和执行计划分为:
# 注意这是优化器改写的SQL,客户端上是不能用 semi join 语法的
select
`t1`.`id`,`t1`.`a`,`t1`.`b`
from `t1` semi join `t2`
where
( (`t1`.`a` = `<subquery2>`.`b`) and (`t2`.`id` < 10) );
# 执行计划
+----+--------------+-------------+-------+---------+---------------+------+-------------+
| id | select_type | table | type | key | ref | rows | Extra |
+----+--------------+-------------+-------+---------+---------------+------+-------------+
| 1 | SIMPLE | <subquery2> | ALL | NULL | NULL | NULL | Using where |
| 1 | SIMPLE | t1 | ref | a | <subquery2>.b | 1 | NULL |
| 2 | MATERIALIZED | t2 | range | PRIMARY | NULL | 9 | Using where |
+----+--------------+-------------+-------+---------+---------------+------+-------------+
semijoin 优化实现比较复杂,其中又分FirstMatch、Materialize 等策略,上面的执行计划中 select_type=MATERIALIZED 就是代表使用了 Materialize 策略来实现的 semijoin。这里 semijoin 优化后的执行流程为:
- 先执行子查询,把结果保存到一个临时表中,这个临时表有个主键用来去重;
- 从临时表中取出一行数据 R;
- 从数据行 R 中,取出字段 b 到被驱动表 t1 中去查找,满足条件则放到结果集;
- 重复执行 2、3,直到结束。
这样一来,子查询结果有9行,即临时表也有9行(这里没有重复值),总的扫描行数为 9+9+9*1=27 行,比原来的 1000 行少了很多。
3、materialization
MySQL5.6版本中加入的另一种优化特性 materialization,就是把子查询结果物化成临时表,然后代入到外查询中进行查找。内存临时表包含主键(hash 索引),消除重复行,使表更小。如果子查询结果太大,超过 tmp_table_size 大小,会退化成磁盘临时表。
这跟前面提到的“我们误以为的”过程相似,这样子查询只需要执行一次,而不是对于外层查询的每一行都得执行一遍。不过要注意的是,这样外查询依旧无法通过索引快速查找到符合条件的数据,只能通过全表扫描或者全索引扫描,
经过 materialization 优化后的执行计划为:
+----+-------------+-------+-------+---------+------+------+-------------+
| id | select_type | table | type | key | ref | rows | Extra |
+----+-------------+-------+-------+---------+------+------+-------------+
| 1 | PRIMARY | t1 | ALL | NULL | NULL | 100 | Using where |
| 2 | SUBQUERY | t2 | range | PRIMARY | NULL | 9 | Using where |
+----+-------------+-------+-------+---------+------+------+-------------+
总扫描行数为 100+9=109.
4、子查询的优化器策略
对于不同类型的子查询,优化器会选择不同的策略。MySQL官方文档
- 对于
IN
、ANY
子查询,优化器有如下策略选择:- semijoin
- Materialization
- exists
- 对于
NOT IN
、<>ALL
子查询,优化器有如下策略选择:- Materialization
- exists
- 对于 derived 派生表,优化器有如下策略选择:
- derived_merge(将派生表合并到外部查询中,MySQL 5.7引入)
- 将派生表物化为内部临时表,再用于外部查询。
优化器策略的选择:
- 对于子查询,先检查是否满足各种优化策略的条件
- update 和 delete 语句中子查询不能使用 semijoin、materialization 优化策略
- 子查询中有 union 则无法使用 semijoin 优化策略
- 然后优化器会按成本进行选择
- 实在没得选就会用 exists 策略来优化子查询
优化器策略的开关:
semijoin 和 materialization 的开关是通过 optimizer_switch 参数中的 semijoin={on|off}、materialization={on|off} 标志来控制的。上文中不同的执行计划就是对 semijoin 和 materialization 进行开/关产生的。
exists 策略是没有参数来开启或者关闭的
# 查看MySQL采用的优化器策略
show variables like 'optimizer_switch';
# 修改优化器策略
set global optimizer_switch='materialization=on,semijoin=on';
set global optimizer_switch='materialization=off,semijoin=off';
5、in 和 exists
-- in子查询,子查询的数据先被查询出来,然后根据子查询的结果集进行主查询
explain select * from employee where dep_id in (select id from department)
-- 子查询获取department表中5条数据,作为外层循环5次
for(select id from department d)
-- 每次循环执行employee表中的查询
select * from employee e where e.dep_id=d.id
-- exits子查询,主查询执行一次(查询e表),子查询依赖主查询获取的数据执行一次(查询d表)
explain select * from employee e
where exists (select 1 from department d where d.id = e.dep_id)
-- 主查询获取employee表中8条数据,作为外层循环8次
for(select * from employee e)
-- 每次循环执行department表中的查询
select 1 from department d where d.id = e.dep_id
结论:小表驱动大表
- 主查询 < 子查询,用
exists
(exists
是外表驱动) - 主查询 > 子查询,用
in
(in
是内表驱动)
另外,exists
子查询只返回true
或false
,因此子查询中的 select *
可以是 select 1
或 其它
6、子查询 优化小结
子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作 。但是子查询的执行效率不高。
-
执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表,然后外层查询语句从临时表中查询记录。
查询完毕后,再撤销这些临时表。这样会消耗过多的CPU和IO资源,产生大量的慢查询。
-
子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表,都不会存在索引,所以查询性能会受到一定的影响。
-
对于返回结果集比较大的子查询,其对查询性能的影响也就越大。
子查询优化建议:
- 使用 连接查询 替代 子查询。
- 连接查询不需要建立临时表,通常比子查询效率更高,尤其是在大型数据集上。
- 使用 EXISTS 替代 IN
- EXISTS只检查是否存在满足条件的行,而IN会在子查询中返回所有匹配项,然后检查是否与主查询中的值匹配。
- 尽量减少子查询的嵌套层级
- 每个嵌套层级都会增加查询的复杂度和执行时间
- 使用合适的索引
结论:尽量不要使用
NOT IN
或者NOT EXISTS
,用LEFT JOIN xxx ON xx WHERE xx IS NULL
替代
八、其他优化
1、几种count性能对比
count()
是一个特殊的函数,有两种非常不同的作用:
- 统计某个列值的数量(要求列值是非空的,即不统计NULL )
- 统计行数(确认括号内的表达式值不可能为空时)
count()
对于返回的结果集,一行行地判断,如果参数不是NULL,累计值就加1,否则不加。最后返回累计值。
# count(主键id)
InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。
server层拿到id后,判断是不可能为空的,直接按行累加。
# count(1)
InnoDB引擎遍历整张表,但不取值。
server层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,直接按行累加。
# count(字段)
not null
一行行地从记录里面读出这个字段,判断不能为null,直接按行累加;
null
判断到有可能是null,还要把值取出来再判断一下,不是null才累加。
# count(*)
它会忽略所有的列而直接统计所有的行数。
MySQL优化器优化了count(*)的语义为“取行数”,count(*)肯定不是null
不取值,且直接按行累加
结论:
- 按照效率排序的话,
count(*) ≈ count(1) > count(主键id) > count(字段)
注意事项:
- 如果采用
count(字段)
,尽量采用二级索引,因为主键采用的是聚簇索引,包含的信息多,明显大于二级索引 - 对于
count(*)
和count(1)
来说,系统会自动采用占用空间更小的二级索引来统计。- 如果有多个二级索引,会使用
ken_len
小的二级索引。 - 如果没有二级索引,才会使用
主键索引
进行统计。
- 如果有多个二级索引,会使用
count(1)
执行得要比count(主键id)
快。因为从引擎返回id会涉及到解析数据行,以及拷贝字段值的操作。
2、别用 select *
在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,推荐使用SELECT <字段列表>查询。原因:
- MySQL 在解析的过程中,会通过 查询数据字典 将
*
按序转换成所有列名,这会大大的耗费资源和时间。 - 无法使用 覆盖索引
- 可能会使排序产生
磁盘临时文件
导致多次I/O。
3、使用 limit1
如果针对的是会全表扫描的 SQL 语句,并且可以确定结果集只有一条,,那么加上 LIMIT 1 的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。
如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上 LIMIT1 了。
4、及时commit
只要有可能,在程序中尽量多使用 COMMIT,这样程序的性能得到提高,需求也会因为 COMMIT 所释放的资源而减少。
COMMIT 所释放的资源:
- 回滚段上用于恢复数据的信息
- 被程序语句获得的锁
- redo / undo log buffer 中的空间
- 管理上述 3 种资源中的内部花费