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, 并作为结果集返回。
驱动表
,b表作为被驱动表
(也可能会被优化为内连接,那就不一定是a为驱动表了)被驱动表
添加索引Using join buffer
的优化
Using join buffer (Block Nested Loop)
Using join buffer (hash join)
对于内连接,例如
a inner join b
驱动表
,大表作为被驱动表
(小表驱动大表)驱动表
,有索引的表作为被驱动表
(有索引的被驱动)Tips:Explain的结果中,上边一行为驱动表,下边一行为被驱动表
从表A取出一条数据,遍历表B,将匹配到的数据放到result(驱动表A中的每一条记录都与被驱动表B的所有记录进行匹配)
这种方式的效率是非常低的,假设A表数据100,B表数据1000条,则 join比较次数 为 A*B=10w
次
lndex Nested-Loop Join 优化的思路主要是为了减少内层表数据的匹配次数,所以要求被驱动表上必须有索引才行。
从表A取出一条数据,通过索引匹配表B中的数据(避免和内层表的每条记录进行比较,极大的减少了和内层表的匹配次数)
驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本比较固定,故mysql优化器都倾向使用记录数少的作为驱动表
如果被驱动表加的索引不是主键索引,还得进行一次回表查询。因此,如果被驱动表的索引是主键索引,效率会更高。
如果存在索引,那么会使用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)
从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 的特点:
小表驱动大表
(本质就是减少外层循环的数据数量)(小表的衡量标准是:表行数*每行大小)
为被驱动表的关联字段创建索引
(减少内层表的循环匹配次数)两张表在 join 时,首先会计算笛卡尔积
-- 使用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的过滤就是最终的。
group by:
1、被分组字段有几类,最终结果集中就有几条(每一类只会显示首条结果)
-- 如sex有男,女两类,结果集就只有2条
2、分组查询中,select之后跟【与分组字段一对一关系的字段】和【聚合函数】
-- 因为只会取首条结果,如果不是一对一的关系,没有意义
3、一般和聚合函数一起使用:
-- 无分组,聚合函数将表中所有符合条件的数据当成一组。
-- 有分组,聚合在分组之后执行,对每一组数据进行聚合
4、分组之后就不需要再去重了:
-- 分组的时候是将列中唯一的值分成一组,这意味着所有的记录都是不同的。
使用Explain分析时,如果Extra出现Using temporary
:
这时候就要注意是否需要对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会很慢。
无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序
或者尽可能避免对大量数据进行排序
。
select city, name, age from t where city='杭州' order by name limit 1000;
当不能使用索引生成排序结果的时候,MySQL需要自己进行排序:
数据量小
:排序在内存中
进行数据量大
:需要使用磁盘
辅助排序。MySQL将这个过程统一称为文件排序
(Using Filesort),即使完全是内存排序不需要任何磁盘文件时也是如此。
先读取查询所需要的所有列,然后再根据指定列进行排序,最后直接返回排序结果。(MySQL 4.1才引入)
单路排序的流程
sort_buffer
,确定放入的字段(要排序的字段 & 要查询的字段
)city索引树
找到所有符合 where条件
的 主键id
。主键索引树
获取 name、city、age三个字段的值,存入sort_buffer中;(回表查询)sort_buffer
中的数据,按照 排序字段name
做快速排序;读取
行指针
和需要排序的字段
,对其进行排序,然后再根据排序结果读取所需要的数据行。
双路排序的流程
sort_buffer
,确定放入的字段(要排序的字段 & 主键id
)city索引树
找到所有符合 where条件
的 主键id
。主键索引树
获取 name、id这两个字段的值,存入sort_buffer中;(回表查询1)sort_buffer
中的数据,按照 排序字段name
做快速排序;主键id
的值,到原表中取出city、name和age三个 要查询的字段 返回给客户端。(回表查询2)【单路排序】
要排序的字段
和 要返回的字段
,放到sort_buffer中。(第1次回表)
外部排序
的概率更大。要查询的字段
。(不需要再回表查询)【双路排序】
要排序的字段
和 主键id
,放到sort_buffer中。(第1次回表)
外部排序
的概率更小。主键id
再 回表查询要返回的字段
。(第2次回表)MySQL选择算法的依据是内存是否足够
,即 单行的长度(要查询的所有列的总长度)是否超过 max_length_for_sort_data
直接返回
查询结果,不用再回到原表去取数据。一次可以排序更多行
,但是需要再回到原表去取数据。这也就体现了MySQL的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问。
MySQL会给每个线程
分配一块内存用于排序,称为 sort_buffer
。
FireSort 可能在内存中完成,也可能需要使用外部排序,这取决于参数 sort_buffer_size
和 排序所需的内存
。
磁盘临时文件
辅助排序。(归并排序)
提高 sort_buffer_size
会降低外部排序的概率,减少磁盘临时文件的生成,减少磁盘I/O。
set sort_buffer_size = 524288;
注意事项:
MySQL在排序时,对每一个排序记录
都会分配一个足够大的定长空间
来存放(根据数据库定义的字段结构大小)
因此,排序消耗的临时空间
可能要比磁盘上的原表要大很多。
如果 要查询的所有列的总长度
超过了 max_length_for_sort_data的大小
,MySQL就会使用双路排序
。
提高 max_length_for_sort_data
会降低改用双路排序
的概率。
set max_length_for_sort_data = 8192;
MySQL之所以需要生成临时表,并且在临时表上做排序操作,是因为原来的数据都是无序的
。如果能够保证从索引树取出的数据,天然就是有序的话,就可以不用再排序了。
InnoDB的索引树 默认就是按照索引字段递增排序的
,因此只要对排序字段加上索引,就可以不用再排序了。
# 创建一个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='杭州'
条件的主键id;主键索引树
获取整行,取name、city、age三个字段的值,作为结果集的一部分直接返回;city='杭州'
条件时循环结束。# 添加覆盖索引进一步简化流程(无需回表查询)
alter table t add index city_user_age(city, name, age);
覆盖索引查询的过程(有序且无需回表)
从索引 (city,name,age) 找到第一个满足 city='杭州'
条件的记录,
取出其中的name、city、age三个字段的值,作为结果集的一部分直接返回;
从索引 (city,name,age) 取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回;
重复执行步骤2,直到查到第1000条记录,或者是不满足 city='杭州'
条件时循环结束。
在关联查询的时候,如果需要排序,MySQL会分两种情况来处理这样的文件排序。
Using filesort
。Using temporary; Using filesort
。如果查询中有 LIMIT 的话,LIMIT 也会在排序之后应用,所以即使需要返回较少的数据,临时表和需要排序的数据量仍然会非常大。
MySQL 5.6 在这里做了很多重要的改进。当只需要返回部分排序结果时,例如使用了LIMIT 子句,MySQL不再对所有的结果进行排序,而是根据实际情况,选择抛弃不满足条件的结果,然后再进行排序。
满足以下条件,MySQL才能在排序的时候使用上索引(Index排序)。
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)
组合索引左边的列必须存在
# 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;
和 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;
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;
查询的数据量很大,并且查询的字段需要回表,优化器会认为回表的代价太大,直接选择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;
# 建立组合索引(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 /* 对于排序来说,多个相等条件也是范围查询 */
使用Explain分析时,如果Extra出现Using filesort
:
这时候就要注意是否需要对order by进行优化了
有 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
这个条件,如果没有用到索引的话,要对几万条的数据进行扫描,这是非常消耗性能的,所以索引放在这个字段上性价比更高,是最优选择。结论:
尽量使用索引优化排序,避免FileSort排序(具体问题具体分析,参考第7点)
磁盘临时文件
导致多次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 改用双路排序
的概率。
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
# 在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容
EXPLAIN SELECT * FROM student t, (SELECT id FROM student ORDER BY id LIMIT 300000,20) a WHERE t.id = a.id;
# 如果是自增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
有以下子查询示例:
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 字段,所以子查询是没法先执行的。执行流程为:
总的扫描行数为 100+100*9=1000(这是理论值,实际值为 964,看规律是子查询结果集每多一行,总扫描行数就会少几行)。
这样会有个问题,如果外层表是一个非常大的表,对于外层查询的每一行,子查询都得执行一次,这个查询的性能会非常差。我们很容易想到将其改写成 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 优化后的执行流程为:
这样一来,子查询结果有9行,即临时表也有9行(这里没有重复值),总的扫描行数为 9+9+9*1=27 行,比原来的 1000 行少了很多。
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.
对于不同类型的子查询,优化器会选择不同的策略。
IN
、ANY
子查询,优化器有如下策略选择:
NOT IN
、<>ALL
子查询,优化器有如下策略选择:
优化器策略的选择:
优化器策略的开关:
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';
-- 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
或 其它
子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作 。但是子查询的执行效率不高。
执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表,然后外层查询语句从临时表中查询记录。
查询完毕后,再撤销这些临时表。这样会消耗过多的CPU和IO资源,产生大量的慢查询。
子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表,都不会存在索引,所以查询性能会受到一定的影响。
对于返回结果集比较大的子查询,其对查询性能的影响也就越大。
子查询优化建议:
结论:尽量不要使用
NOT IN
或者NOT EXISTS
,用LEFT JOIN xxx ON xx WHERE xx IS NULL
替代
count()
是一个特殊的函数,有两种非常不同的作用:
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会涉及到解析数据行,以及拷贝字段值的操作。在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,推荐使用SELECT <字段列表>查询。原因:
*
按序转换成所有列名,这会大大的耗费资源和时间。磁盘临时文件
导致多次I/O。如果针对的是会全表扫描的 SQL 语句,并且可以确定结果集只有一条,,那么加上 LIMIT 1 的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。
如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上 LIMIT1 了。
只要有可能,在程序中尽量多使用 COMMIT,这样程序的性能得到提高,需求也会因为 COMMIT 所释放的资源而减少。
COMMIT 所释放的资源:
因篇幅问题不能全部显示,请点此查看更多更全内容