目录
简介
通过使用 EXPLAIN 关键字可以模拟优化器执行 SQL 查询语句,从而知道MySQL 是如何处理 SQL 语句。分析查询语句或是表结构的性能瓶颈,总的来说通过 EXPLAIN 可以获取以下信息:
- 表的读取顺序
- 数据读取操作的操作类型
- 哪些索引可以使用
- 哪些索引被实际使用
- 表之间的引用
- 每张表有多少行被优化器查询
注意:如果 from 中包含子查询,仍会执行该子查询,将结果放入临时表中
mysql官方文档:https://dev.mysql.com/doc/refman/8.0/en/explain-output.html
语法
执行计划的语法其实非常简单: 在 SQL 查询的前面加上 EXPLAIN 关键字就行。比如:
EXPLAIN select * from table1
重点的就是 EXPLAIN 后面你要分析的 SQL 语句
除了以 SELECT 开头的查询语句,其余的 DELETE、INSERT、REPLACE 以 及 UPOATE 语句前边都可以加上 EXPLAIN,用来查看这些语句的执行计划,不过对于增删改的语句优化有限,因此后边只会以 SELECT 语句为例来描述 EXPLAIN 语句的用法
返回值含义
- id: 在一个大的查询语句中每个 SELECT 关键字都对应一个唯一的 id
- select_type: SELECT 关键字对应的那个查询的类型
- table:表名
- partitions:匹配的分区信息
- type:针对单表的访问方法
- possible_keys:可能用到的索引
- key:实际上使用的索引
- key_len:实际使用到的索引长度
- ref:当使用索引列等值查询时,与索引列进行等值匹配的对象信息
- rows:预估的需要读取的记录条数
- filtered:某个表经过搜索条件过滤后剩余记录条数的百分比
- Extra:—些额外的信息
示例表DDL
-- 普通示例表
DROP TABLE IF EXISTS `actor`;
CREATE TABLE `actor` (
`id` int(11) NOT NULL,
`name` varchar(45) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `actor` (`id`, `name`, `update_time`) VALUES (1,'a',now()), (2,'b',now()), (3,'c',now());
DROP TABLE IF EXISTS `film`;
CREATE TABLE `film` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(10) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`),
KEY `idx_create` (`create_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3;
INSERT INTO `test`.`film` (`id`, `name`, `create_time`) VALUES (1, 'a', '2020-10-12 12:21:32');
INSERT INTO `test`.`film` (`id`, `name`, `create_time`) VALUES (2, 'b', '2020-10-12 12:21:12');
INSERT INTO `test`.`film` (`id`, `name`, `create_time`) VALUES (3, 'c', '2020-10-12 12:21:00');
CREATE TABLE `film1` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(10) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`),
KEY `idx_create` (`create_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3;
INSERT INTO `test`.`film1` (`id`, `name`, `create_time`) VALUES (1, 'a', '2020-10-12 12:21:32');
INSERT INTO `test`.`film1` (`id`, `name`, `create_time`) VALUES (2, 'b', '2020-10-12 12:21:12');
INSERT INTO `test`.`film1` (`id`, `name`, `create_time`) VALUES (3, 'c', '2020-10-12 12:21:00');
DROP TABLE IF EXISTS `film_actor`;
CREATE TABLE `film_actor` (
`id` int(11) NOT NULL,
`film_id` int(11) NOT NULL,
`actor_id` int(11) NOT NULL,
`remark` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_film_actor_id` (`film_id`,`actor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `film_actor` (`id`, `film_id`, `actor_id`, `remark`) VALUES (1,1,1,'1'),(2,2,2,'2'),(3,3,3,'3');
-- 用于测试myisam特性相关示例表
CREATE TABLE `test_myisam` (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`test_value` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '测试数据',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `test`.`test_myisam` (`id`, `test_value`) VALUES (1, 'test');
-- 用于测试innodb特性相关示例表
CREATE TABLE `test_innodb` (
`id` int NOT NULL COMMENT '主键',
`test_value` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '测试值',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `test`.`test_innodb` (`id`, `test_value`) VALUES (1, 'test');
show warnings
show warnings 命令可以得到优化后的查询语句,从而看出优化器优化了什么
explain select * from actor;
show warnings;
explain变种
以下的两个变种只能在mysql8之前使用,因为mysql5.7之后就会把它们的相关返回的字段集成到explain中,而这两个命令被删除了
- explain extended
会在 explain 的基础上额外提供一些查询优化的信息。紧随其后通过 。额外还有 filtered 列,是一个半分比的值,rows * filtered/100 可以估算出将要和 explain 中前一个表进行连接的行数(前一个表指 explain 中的id值比当前表id值小的表)
- explain partitions
相比 explain 多了个 partitions 字段,如果查询是基于分区表的话,会显示查询将访问的分区
执行计划返回值详解
如果查询的结果不符合当前sql的定义,尝试查看优化器优化后的sql
为了证明部分猜想,需要结合优化器的开关进行测试,具体命令如下:
SELECT @@optimizer_switch; -- 查看所有的优化器开关
默认结果如下:
index_merge=on -- 索引合并开关
index_merge_union=on -- 索引合并-OR(并集)开关
index_merge_sort_union=on -- 索引合并-OR(排序并集)操作开关,不满足union的情况下使用这个
index_merge_intersection=on -- 索引合并-AND(交集)操作开关
engine_condition_pushdown=on -- 引擎条件下推开关(仅适用于NDB存储引擎的非索引列)
index_condition_pushdown=on -- 索引条件下推开关(仅适用于索引列)
mrr=on -- 多范围读取开关
mrr_cost_based=on -- 多范围读取优化是否基于成本计算的开关
block_nested_loop=on -- 是否开启嵌套循环优化
batched_key_access=off -- 是否开启批量Key访问优化
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_indexes=off -- 是否使用不可见索引,用于测试排除无效索引
skip_scan=on -- 是否开启跳表扫描优化,用于联合逐渐索引对于不满足最左前缀的优化
hash_join=on -- 是否开启hash连接的优化,用于对于等值表连接的普通列进行hash优化
subquery_to_derived=off -- 是否开启子查询物化开关
prefer_ordering_index=on -- 是否开启优先使用排序字段作为索引
hypergraph_optimizer=off -- 是否开启超图优化,用于两个集合(超图节点的集合)连接优化查询
derived_condition_pushdown=on -- 是否开启派生条件下推,用于将外层条件判断转移到派生表中,减少派生表扫描行数和返回给外层的数据量
可以根据下面sql进行优化器的开关,以物化合并举例:
set session optimizer_switch='derived_merge=off'; -- 关闭物化合并的优化
set session optimizer_switch='derived_merge=on'; -- 开启物化合并的优化
id
id列的编号是 select 的序列号,有几个 select 就有几个id,并且id的顺序是按 select 出现的顺序增长的
id列越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行
单select关键字
explain select * from film where `name` = 'a';
这个查询中只有一个 SELECT 关键字,所以 EXPLAIN 的结果中也就只有一条 id 列为 1 的记录
连接查询
对于连接查询来说,一个 SELECT 关键字后边的 FROM 子句中可以跟随多个表,所以在连接查询的执行计划中,每个表都会对应一条记录,但是这些记录的 id 值都是相同的,如下所示:
explain select c.`name` from film a left join film_actor b on a.id = b.film_id left join actor c on b.actor_id = c.id where a.`name` = 'a';
子查询
对于包含子查询的查询语句来说,就可能涉及多个 SELECT 关键字,所以在包含子查询的查询语句的执行计划中,每个 SELECT 关键字都会对应一个唯一的 id 值,如下所示:
explain select * from actor where id = (select actor_id from film_actor where film_id = (select id from film where `name` = 'a'));
但是对于子查询可能存在sql优化成为连接查询,执行计划也就会变成连接查询的结果,具体优化情况可使用show warnings查看
UNION查询
UNION 子句会把多个查询 的结果集合并起来并对结果集中的记录进行去重,去重使用的是内部的临时表,如下所示:
explain select * from actor where id = 1 union select * from actor where id = 2;
UNION 子句是为了把 id 为 1 的查询和id为2的查询的结果集合并起来并去重,所以在内部创建了一个名为<union1,2>的临时表(就是执行计划第三条记录的 table 列的名称),id 为 NULL 表明这个临时表是为了合并两个查询的结果集而创建的
UNION ALL 就不需要为最终的结果集进行去重,它只是单纯的把多个查询的结果集中的记录合并成一个并返回给用户,所以也就不需要使用临时表。所以在包含 UNION ALL 子句的查询的执行计划中,就没有那个 id 为 NULL 的记录,如下所示:
explain select * from actor where id = 1 union all select * from actor where id = 2;
select_type
代表当前查询的复杂性
SIMPLE
简单的 select 查询,不使用 union 及子查询
explain select * from film where `name` = 'a';
explain select c.`name` from film a left join film_actor b on a.id = b.film_id left join actor c on b.actor_id = c.id where a.`name` = 'a';
PRIMARY
复杂查询中最外层的 select 查询
对于包含 UNION、UNION ALL 或者子查询的大查询来说,它是由几个小查询组成的,其中最左边的那个查询的 select_type 值就是 PRIMARY,例如:
explain select * from actor where id = 1 union all select * from actor where id = 2;
UNION
UNION 中的第二个或随后的 select 查询,不依赖于外部查询的结果集
explain select * from actor where id = 1 union all select * from actor where id = 2;
UNION RESULT
UNION 结果集
explain select * from actor where id = 1 union select * from actor where id = 2;
SUBQUERY
子查询中的 select 查询,不依赖于外部查询的结果集
如果包含子查询的查询语句不能够转为对应的 semi-join 的形式,并且该子查询是不相关子查询,并且查询优化器决定采用将该子查询物化的方案来执行该子查询时,该子查询的第一个 SELECT 关键字代表的那个查询的 select_type 就是 SUBQUERY
explain select * from actor where id = (select actor_id from film_actor where film_id = (select id from film where `name` = 'a'));
外层查询的 select_type 就是 PRTIMARY,子查询的 select_type 就是 SUBQUERY。需要注意的是,由于 select_type 为 SUBQUERY 的子查询由于会被物化,所以只需要执行一遍
semi-join:半连接优化技术,本质上是把子查询上拉到父查询中,与父查询的表做 join 操作。关键词是“上拉”。对于子查询,其子查询部分相对于父表的每个符合条件的元组,都要把子查询执行一轮。效率低下。用半连接操作优化子查询,是把子查询上拉到父查询中,这样子查询的表和父查询中的表是并列关系,父表的每个符合条件的元组,只需要在子表中找符合条件的元组即可。简单来说,就是通过将子查询上拉对父查询中的数据进行筛选,以使获取到最少量的足以对父查询记录进行筛选的信息就足够了
子查询物化:子查询的结果通常缓存在内存或临时表中
关联/相关子查询:子查询的执行依赖于外部查询。多数情况下是子查询的 WHERE 子句中引用了外部查询的表。自然“非关联/相关子查询”的执行则不依赖与外部的查询
DEPENDENT UNION/DEPENDENT SUBQUERY
DEPENDENT UNION 代表 UNION 中的第二个或随后的 select 查询,外部依赖内部查询的结果集
DEPENDENT SUBQUERY 代表子查询中的第一个 select 查询,外部依赖内部查询的结果集
explain select * from film where id in(select id from film union select id from film);
DERIVED
对于采用物化的方式执行的包含派生表的查询,该派生表对应的子查询的 select_type 就是 DERIVED
explain select t.* from (select id, count(1) from actor group by id) t where t.id != 2;
这种类型一旦出现,就代表当前查询是通过物化的方式来实现的
MATERIALIZED
物化子查询,子查询来自于视图,mysql的各种优化器都会对这种情况进行优化,因此极少出现
UNCACHEABLE SUBQUERY
结果集不能被缓存的子查询,必须重新为外层查询的每一行进行评估,出现极少
UNCACHEABLE UNION
UNION 中的第二个或随后的 select 查询,属于不可缓存的子查询,出现极少
table
无论sql语句多么复杂,最后都是拆分成为多个单表的操作,因此,执行计划中的每条记录都会对应一个表名
当 from 子句中有子查询时,table列是 格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查询
当有 union 时,UNION RESULT 的 table 列的值为<union1,2>,1和2表示参与 union 的 select 行id
具体示例可参考上面的id详解示例
partitions
和分区表有关,一般情况下我们的查询语句的执行计划的 partitions 列的值都是 NULL
type
这一列表示关联类型或访问类型,即MySQL决定如何查找表中的行,查找数据行记录的大概范围
依次从最优到最差分别为:Null > system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
一般来说,得保证查询达到range级别,最好达到ref
Null
mysql能够在优化阶段分解查询语句,在执行阶段用不着再访问表或索引。例如:在索引列中选取最小值,可以单独查找索引来完成,不需要在执行时访问表
explain select min(id) from film;
system
当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如 MyISAM、Memory,那么对该表的访问方法就是 system
explain select * from test_myisam;
如果当前表的数据不是1条或者存储引擎是innodb,则type就不是system,如下所示:
explain select * from test_innodb;
const
根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是 const。因为只匹配一行数据,所以很快
explain select * from film where id = 1;
MySQL 把这种通过主键或者唯一二级索引列来定位一条记录的访问方法定义为:const,意思是常数级别的,代价是可以忽略不计的
注意:
- 如果是联合唯一二级索引,则需要所有的字段都命中,才能定义为const
- 如果唯一二级索引可以为空,并且使用is null来获取数据时,不会是const,因为null可以存在多个
eq_ref
在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较),则对该被驱动表的访问方法就是 eq_ref
explain select * from film_actor left join film on film_actor.film_id = film.id;
注意:非连接查询不会出现这种情况
ref
当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是 ref
- 简单 select 查询,name是普通索引(非唯一索引)
explain select * from film where name = 'film1';
- 关联表查询,idx_film_actor_id是film_id和actor_id的联合索引,这里使用到了film_actor的左边前缀film_id部分
explain select film_id from film left join film_actor on film.id = film_actor.film_id;
fulltext
全文索引相关,在mysql中基本上这个很少使用,主要由于他不支持中文分词,并且还有比他更为优秀和专业的替代品
ref_or_null
不仅通过二级索引与常量进行对比,而且对其为空的数据也进行获取
explain select * from film where name = 'film1' or name is null;
index_merge
索引合并优化的体现,简单来说就是一般情况下一条sql只会使用到一个索引,但在特殊情况下,触发索引合并就会使用到多个索引
explain select * from film where name = 'film1' or create_time = '2020-10-12 12:21:00';
unique_subquery
unique _subquery 是针对在一些包含IN子查询的查询语句中,如果查询优化器决定将IN子查询转换为EXISTS 子查询,而且子查询可以使用到主键进行等值匹配的话,那么该子查询执行计划的 type 列的值就是 unique_subquery
explain select * from film where id in (select id from film1 where film.create_time = film1.create_time) or name = 'a';
index_subquery
index_subquery 与 unique_subquery 类似,只不过访问⼦查询中的表时使⽤的是普通的索引
explain select * from film where name in (select name from film1 where film.create_time = film1.create_time) or id = 1;
range
如果使用索引获取某些范围区间的记录,那么就可能使用到range访问方法,一般就是在你的 where 语句中出现了 between、<、>、in 等的查询
explain select * from film where name in ('a', 'b');
explain select * from actor where id > 1;
index
扫描全索引就能拿到结果,一般是扫描某个二级索引,这种扫描不会从索引树根节点开始快速查找,而是直接对二级索引的叶子节点遍历和扫描,速度还是比较慢的,这种查询一般为使用覆盖索引,二级索引一般比较小,所以这种通常比ALL快一些
explain select id, name from film;
ALL
即全表扫描,扫描你的聚簇索引的所有叶子节点。通常情况下这需要增加索引来进行优化了
explain select * from film;
possible_keys
这一列显示查询可能使用哪些索引来查找
explain 时可能出现 possible_keys 有数据,而 key 显示 NULL 的情况,这种情况是因为表中数据不多,mysql认为索引对此查询帮助不大,选择了全表查询
如果该列是NULL,则没有相关的索引。在这种情况下,可以通过检查 where 子句看是否可以创造一个适当的索引来提高查询性能,然后用 explain 查看效果
注意:
- 覆盖索引获取数据比较特殊,可能possible_keys显示null,但是最后会用到索引
- 如果possible_keys中可能用到的索引比较多,会导致成本分析的时间过长,需要删除冗余索引
key
这一列显示mysql实际采用哪个索引来优化对该表的访问
如果没有使用索引,则该列是 NULL。如果想强制mysql使用或忽视possible_keys列中的索引,在查询中使用 force index、ignore index
下面例子根据主键查询,默认走主键索引
explain select * from film where id = 1 and name = 'a';
强制忽略索引
explain select * from film ignore index(PRIMARY) where id = 1 and name = 'a';
强制使用索引
explain select * from film force index(idx_name) where id = 1 and name = 'a';
key_len
这一列显示了mysql在索引里使用的字节数,通过这个值可以算出具体使用了索引中的哪些列
key_len计算规则如下:
- 字符串,char(n)和varchar(n),5.0.3以后版本中,**n均代表字符数,而不是字节数,**如果是utf-8,一个数字或字母占1个字节,一个汉字占3个字节
- char(n):如果存汉字长度就是 3n 字节
- varchar(n):如果存汉字则长度是 3n + 2 字节,加的2字节用来存储字符串长度,因为varchar是变长字符串
- 数值类型
- tinyint:1字节
- smallint:2字节
- int:4字节
- bigint:8字节
- 时间类型
- date:3字节
- timestamp:4字节
- datetime:8字节
- 如果字段允许为 NULL,需要1字节记录是否为 NULL
索引最大长度是768字节,当字符串过长时,mysql会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索引
film_actor的联合索引 idx_film_actor_id 由 film_id 和 actor_id 两个int列组成,并且每个int是4字节。通过结果中的key_len=4可推断出查询使用了第一个列:film_id列来执行索引查找
explain select * from film_actor where film_id = 2;
但如果两个列都使用,则如下所示:
explain select * from film_actor where film_id = 2 and actor_id = 2;
ref
这一列显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例:film.id),null
const
explain select * from film_actor where film_id = 2 and actor_id = 2;
字段名
explain select * from film where id in (select id from actor);
null
explain select * from film where create_time > now();
rows
这一列是mysql估计要读取并检测的行数,注意这个不是结果集里的行数
比如根据二级索引获取数据,预估扫描只有一条记录
explain select * from film where name = 'a';
不管是innodb还是myisam,对于扫描记录都是基于索引数据的统计,不同的是,innodb的统计值是通过算法预估出来的,而myisam是精确统计
filtered
查询优化器预测有多少条记录满⾜其余的搜索条件,也就是预估结果数量与扫描数量的比值,最大100
explain select * from film where name = 'a';
上面的例子可以看出结果数量与扫描行数的比值为100,默认也就是完全匹配,一般此字段用于代表驱动表的扇出数
Extra
Extra 列是用来说明一些额外信息的,可以通过这些额外信息来更准确的理解 MySQL 到底将如何执行给定的查询语句。MySQL 提供的额外信息很多,常见的如下
No tables used
当查询语句的没有 FROM 子句时将会提示该额外信息
explain select now();
Impossible WHERE
查询语句的 WHERE 子句永远为 FALSE 时将会提示该额外信息
explain select * from film where 1 != 1;
No matching min/max row
当查询使用 MIN 或者 MAX 聚集函数,但是并没有符合 WHERE 子句中的搜索条件的记录时,将会提示该额外信息
explain select max(id) from film where name = 'f';
Select tables optimized away
使用某些聚合函数来访问存在索引的某个字段
explain select min(id) from film;
Using index
使用覆盖索引
覆盖索引定义:mysql执行计划explain结果里的key有使用索引,如果select后面查询的字段都可以从这个索引的树中获取,这种情况一般可以说是用到了覆盖索引,extra里一般都有using index;覆盖索引一般针对的是辅助索引,整个查询结果只通过辅助索引就能拿到结果,不需要通过辅助索引树找到主键,再通过主键去主键索引树里获取其它字段值
explain select actor_id from film_actor where film_id = 1;
Using index condition
查询的列不完全被索引覆盖,where条件中是一个前导列的范围(使用了索引下推),简单说就是存储引擎尽量利用索引进行过滤,然后在返回需要的数据
explain select * from film_actor where film_id > 1;
索引下推:根据二级索引查询到的结果,再通过存储引擎来尽量过滤来达到减少回表次数的优化
一般出现这种提示需要考虑索引未被充分利用的情况
Using where
代表mysql使用where后面的条件对数据进行了过滤,也就是说存在没有用到索引的情况
explain select * from film_actor where actor_id = 1;
Using join buffer (Block Nested Loop)
在连接查询中,驱动表无法有效利用索引而导致的嵌套循环
explain select * from film_actor a left join actor b on a.actor_id = b.name;
Not exists
在指定驱动表(left join、right join)的连接中,如果被驱动表的筛选条件为空,但当前列不允许为空,则会出现这种类型
explain select * from film_actor a left join actor b on a.actor_id = b.id where b.id is null;
索引合并相关
代表将会使用索引合并的方式进行查询,括号中就是将要合并的索引,总共分为下面三类:
- Using intersect(…):使用交集的方式进行索引合并
- Using union(…):使用索引列并集的方式进行索引合并
- Using sort_union(…):未命中索引列并集的方式进行索引合并
对于索引合并,会有单独说明
Zero limit
这个代表了limit参数为0的情况
explain select * from film limit 0;
Using filesort
代表当前的排序使用了文件排序(内存或磁盘),没有有效的利用索引的有序性
数据较小时从内存排序,否则需要在磁盘完成排序,这种情况下一般也是要考虑使用索引来优化的
explain select * from film where id != 1 order by name;
需要注意的是,如果查询中需要使用 filesort 的方式进行排序的记录非常多,那么这个过程是很耗费性能的,最好想办法将使用文件排序的执行方式改为使用索引进行排序
Using temporary
使用临时表来进行查询的聚合操作,常见的有DISTINCT、GROUP BY、UNION等
explain select distinct name from actor;
在这儿需要注意的是GROUP BY a默认代表GROUP BY a ORDER BY a,因此一般伴随着Using filesort,但在mysql8中对此进行了修改:
explain select name, count(1) from actor group by name;
mysql8以前:
如果需要指定不排序,则显示的使用GROUP BY a ORDER BY null即可
mysql8以后:
Start temporary, End temporary
有子查询时,查询优化器会优先尝试将 IN 子查询转换成 semi-join(半连接优化技术,本质上是把子查询上拉到父查询中,与父查询的表做 join 操作),而 semi-join 又有好多种执行策略,当执行策略为 DuplicateWeedout(临时表去重优化) 时,也就是通过建立临时表来实现为外层查询中的记录进行去重操作时,驱动表查询执行计划的 Extra 列将显示 Start temporary 提示,被驱动表查询执行计划的 Extra 列将显示 End temporary 提示
explain select * from film where id in(select name from actor);
对于临时表去重的操作,主要是查询出来的子数据需要去重返回,但有没有唯一性的数据支撑,因此使用临时表来做去重操作
LooseScan
在将 In 子查询转为 semi-join 时,如果采用的是 LooseScan 执行策略,则在驱动表执行计划的 Extra 列就是显示 LooseScan 提示,LooseScan代表的是松散扫描
explain select * from film where id in(select film_id from film_actor where film_id = 1 or actor_id = 2);
松散扫描是需要将子查询的数据进行分组来达到去重效果
FirstMatch(tbl_name)
在将 In 子查询转为 semi-join 时,如果采用的是 FirstMatch 执行策略,则在被驱动表执行计划的 Extra 列就是显示 FirstMatch(tbl_name)提示,FirstMatch代表首次匹配
explain select * from film where id in(select film_id from film_actor where film_id = 1);
首次匹配其实就是对于连接查询中的被驱动表的去重操作,他的去重方式就是按任意顺序找到第一个即可
MRR
MRR全称为Disk-Sweep Multi-Range Read,即先读取一部分二级索引记录,将它们的主键值排好序之后再统一执行回表操作
相对于每读取一条二级索引记录就立即执行回表操作,这样会节省一些 IO 开销。使用这个 MRR 优化措施的条件比较苛刻,所以我们直接认为每读取一条二级索引记录就立即执行回表操作
由于mrr会基于成本计算导致是否使用mrr不可控,因此,我们关闭基于成本计算来做mrr优化的开关:
set session optimizer_switch='mrr_cost_based=off'; -- 关闭基于成本计算来选择是否使用mrr的优化
测试mrr:
explain select * from film_actor where film_id > 0 and film_id < 4;
默认还是需要开启基于成本的mrr优化,因为在数据量不大的情况下,这个优化的作用不是很大,反而会多增加一些处理,开启方式为:
set session optimizer_switch='mrr_cost_based=on'; -- 开启基于成本计算来选择是否使用mrr的优化