MySQL优化之EXPLAN执行计划
** 备注 **
本文改编自https://www.processon.com/view/5d4fe8f4e4b04399f5a0303e?fromnew=1#map
待阅读完千金良方MySQL后对此文进行二次优化
本次案例使用到的SQL
--建立下面格式的表S1,S2结构
CREATE TABLE single_table (
id INT NOT NULL 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),
KEY idx_key1 (key1),
UNIQUE KEY idx_key2 (key2),
KEY idx_key3 (key3),
KEY idx_key_part(key_part1, key_part2, key_part3)
) Engine=InnoDB CHARSET=utf8;
--说明:
--主键id列
--二级索引列 key1,key2(唯一二级),key3
--联合索引列 (keypart1, keypart2, keypart3))
--普通列commonfield
--使用存储过程插入10000条语句
CREATE DEFINER=`root`@`localhost` PROCEDURE `InsertRecordS1`()
BEGIN
DECLARE count int default 1;
WHILE count<=10000 DO
insert into s1 (id,key1,key2,key3,key_part1,key_part2,key_part3,common_field) values (null,CONCAT('key1',count),count,CONCAT('key3',count),CONCAT('key_part1',count),CONCAT('key_part2',count),CONCAT('key_part3',count),CONCAT('common',count));
set count=count+1;
END WHILE;
END
call InsertRecordS1();
实际用到的SQL
```sql
--创建测试用表s1和s2;
CREATE TABLE s1 (
id INT NOT NULL 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),
KEY idx_key1 (key1),
UNIQUE KEY idx_key2 (key2),
KEY idx_key3 (key3),
KEY idx_key_part(key_part1, key_part2, key_part3)
) Engine=InnoDB CHARSET=utf8;
CREATE TABLE s2 (
id INT NOT NULL 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),
KEY idx_key1 (key1),
UNIQUE KEY idx_key2 (key2),
KEY idx_key3 (key3),
KEY idx_key_part(key_part1, key_part2, key_part3)
) Engine=InnoDB CHARSET=utf8;
--使用存储过程插入10000条语句
delimiter //
CREATE DEFINER=`root`@`localhost` PROCEDURE `InsertRecordS1`()
BEGIN
DECLARE count int default 1;
WHILE count<=10000 DO
insert into s1 (id,key1,key2,key3,key_part1,key_part2,key_part3,common_field) values (null,CONCAT('key1',count),count,CONCAT('key3',count),CONCAT('key_part1',count),CONCAT('key_part2',count),CONCAT('key_part3',count),CONCAT('common',count));
set count=count+1;
END WHILE;
END
//
delimiter ;
call InsertRecordS1();
delimiter //
CREATE DEFINER=`root`@`localhost` PROCEDURE `InsertRecordS2`()
BEGIN
DECLARE count int default 1;
WHILE count<=10000 DO
insert into s2 (id,key1,key2,key3,key_part1,key_part2,key_part3,common_field) values (null,CONCAT('key1',count),count,CONCAT('key3',count),CONCAT('key_part1',count),CONCAT('key_part2',count),CONCAT('key_part3',count),CONCAT('common',count));
set count=count+1;
END WHILE;
END
//
delimiter ;
call InsertRecordS2();
使用方式
在查询语句前面加上EXPLAIN关键字
EXPLAIN SELECT * FROM tablename;
各字段含义
名称
解释
id
在一个大的查询语句中每个SELECT关键字都对应一个唯一的ID
select_type
SELECT关键字对应的那个查询的类型
type
单表访问的方法
table
表名
partitions
匹配的分区信息(暂时不涉及)
possible_keys
可能用到的索引
key
实际上使用的索引
key_len
实际使用到的索引长度
ref
当使用索引列等值查询时,与索引列进行等值匹配的对象信息 比如常量const,函数func,或者是驱动表的主键
rows
预估的需要读取的记录数
filtered
某个表经过搜索条件过滤后剩余记录条数的百分比 (扇出率)
Extra
一些额外信息
id列详解
查询语句中每出现一个SELECT关键字,MySQL就会为它分配一个唯一的ID值
注意点
对于连接查询来说,一个SELECT关键字后边的FROM子句中可以跟随多个表, 所以在连接查询的执行计划中,每个表都会对应一条记录,但是这些记录的id值都是相同的
在连接查询的执行计划中,每个表都会对应一条记录,这些记录的id列的值是相同的, 出现在前边的表表示驱动表,出现在后边的表表示被驱动表。
查询优化器可能对涉及子查询的查询语句进行重写,从而转换为连接查询。 那么这时候他们的ID值就一样了啦
对于UNION查询产生的临时表的表示
mysql> explain select * from s1 union select * from s2;
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| 1 | PRIMARY | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 9923 | 100.00 | NULL |
| 2 | UNION | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 9903 | 100.00 | NULL |
| NULL | UNION RESULT | | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
3 rows in set, 1 warning (0.00 sec)
备注
MySQL使用的是内部的临时表。正如上边的查询计划中所示,UNION子句是为了把id为1的查询和id为2的查询的结果集合并起来并去重,所以在内部创建了一个名为的临时表(就是执行计划第三条记录的table列的名称),id为NULL表明这个临时表是为了合并两个查询的结果集而创建的。
select_type列详解
取值范围
名称
解释
SIMPLE
Simple SELECT (not using UNION or subqueries)
PRIMARY
Outermost SELECT
UNION
Second or later SELECT statement in a UNION
UNION RESULT
Result of a UNION
SUBQUERY
First SELECT in subquery
DEPENDENT SUBQUERY
First SELECT in subquery, dependent on outer query
DEPENDENT UNION
Second or later SELECT statement in a UNION, dependent on outer query
DERIVED
Derived table
MATERIALIZED
Materialized subquery
UNCACHEABLE SUBQUERY
A subquery for which the result cannot be cached and must be re-evaluated for each row of the outer query
UNCACHEABLE UNION
The second or later select in a UNION that belongs to an uncacheable subquery (see UNCACHEABLE SUBQUERY)
各取值范围详解
SIMPLE
查询语句中不包含UNION或者子查询的查询
当然连接查询也算是SIMPLE类型
PRIMARY
对于包含UNION、UNION ALL或者子查询的大查询来说,它是由几个小查询组成的, 其中最左边的那个查询的select_type值就是PRIMARY
英文解释直译过来也是 最外层的查询
UNION
对于包含UNION或者UNION ALL的大查询来说,它是由几个小查询组成的, 其中除了最左边的那个小查询以外,其余的小查询的select_type值就是UNION
UNION_RESULT
MySQL选择使用临时表来完成UNION查询的去重工作, 针对该临时表的查询的select_type就是UNION RESULT
SUBQUERY
如果包含子查询的查询语句不能够转为对应的semi-join的形式,并且该子查询是不相关子查询,并且查询优化器决定采用将该子查询物化的方案来执行该子查询时,该子查询的第一个SELECT关键字代表的那个查询的select_type就是SUBQUERY
由于select_type为SUBQUERY的子查询由于会被物化,所以只需要执行一遍
DEPENDENT SUBQUERY
如果包含子查询的查询语句不能够转为对应的semi-join的形式,并且该子查询是相关子查询, 则该子查询的第一个SELECT关键字代表的那个查询的select_type就是DEPENDENT SUBQUERY
select_type为DEPENDENT SUBQUERY的查询可能会被执行多次。
DEPENDENT UNION
在包含UNION或者UNION ALL的大查询中,如果各个小查询都依赖于外层查询的话, 那除了最左边的那个小查询之外,其余的小查询的select_type的值就是DEPENDENT UNION。
DERIVED
对于采用物化的方式执行的包含派生表的查询,该派生表对应的子查询的select_type就是DERIVED
MATERIALIZED
当查询优化器在执行包含子查询的语句时,选择将子查询物化之后与外层查询进行连接查询时, 该子查询对应的select_type属性就是MATERIALIZED
UNCACHEABLE SUBQUERY
并不常用
UNCACHEABLE UNION
并不常用
table列详解
查询哪个表
partitions列详解
暂时用不上
type列详解
取值范围
system
当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如MyISAM、Memory,那么对该表的访问方法就是system。
当表中只有一条记录并且该表使用的存储引擎的统计数据是不精确的,比如InnoDB,那么对该表的访问方法就是全表扫描。
const
根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是const
eq_ref
在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较),则对该被驱动表的访问方法就是eq_ref
ref
当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是ref
ref_or_null
当对普通二级索引进行等值匹配查询,该索引列的值也可以是NULL值时,那么对该表的访问方法就可能是ref_or_null
index_merge
使用Intersection、Union、Sort-Union这三种索引合并的方式来执行查询
unique_subquery
类似于两表连接中被驱动表的eq_ref访问方法,unique_subquery是针对在一些包含IN子查询的查询语句中,如果查询优化器决定将IN子查询转换为EXISTS子查询,而且子查询可以使用到主键进行等值匹配的话,那么该子查询执行计划的type列的值就是unique_subquery
index_subquery
index_subquery与unique_subquery类似,只不过访问子查询中的表时使用的是普通的索引,比如这样:
range
如果使用索引获取某些范围区间的记录,那么就可能使用到range访问方法
index
当我们可以使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是index
ALL
熟悉的全表扫描
fulltext
全文索引,支持一般,专业人干专业事,建议上ES或者Solr
possible_keys与key列详解
possible_keys列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些,key列表示实际用到的索引有哪些
key_len列详解
key_len列表示当优化器决定使用某个索引执行查询时,该索引记录的最大长度
作用
在使用联合索引的时候就可以知道用了哪几列啦
计算规则
对于使用固定长度类型的索引列来说,它实际占用的存储空间的最大长度就是该固定值,对于指定字符集的变长类型的索引列来说,比如某个索引列的类型是VARCHAR(100),使用的字符集是utf8,那么该列实际占用的最大存储空间就是100 × 3 = 300个字节。
如果该索引列可以存储NULL值,则key_len比不可以存储NULL值时多1个字节。
对于变长字段来说,都会有2个字节的空间来存储该变长列的实际长度。
举一个栗子
ref列详解
ref列展示的就是与索引列作等值匹配的东东是个啥,比如只是一个常数或者是某个列。
常数const的栗子
某一列的栗子
函数func的栗子
rows列详解
如果查询优化器决定使用全表扫描的方式对某个表执行查询时,执行计划的rows列就代表预计需要扫描的行数, 如果使用索引来执行查询时,执行计划的rows列就代表预计扫描的索引记录行数。
filtered列详解
驱动表扇出的比例,当然是越低越好啦
Extra列详解
常见取值
No tables used
当查询语句的没有FROM子句时将会提示该额外信息
Impossible WHERE
查询语句的WHERE子句永远为FALSE时将会提示该额外信息
No matching min/max row
当查询列表处有MIN或者MAX聚集函数,但是并没有符合WHERE子句中的搜索条件的记录时,将会提示该额外信息
Using index
当我们的查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用索引覆盖(bu)的情况下,在Extra列将会提示该额外信息
Using index condition
如果在查询语句的执行过程中将要使用索引条件下推这个特性,在Extra列中将会显示Using index condition
什么是索引下推
SELECT * FROM s1 WHERE key1 > 'z' AND key1 LIKE '%a';
对于上面的这条SQL语句,因为这时候走的是以range访问方式走的key1索引。 而key1 LIKE '%a'由于不满足前缀匹配,所以需要回表访问的时候再过滤
可是这个条件列又刚好是key1索引列。如果在二级索引的时候,就进行过滤条件判断,不放到回表后判断。 那么就可以减少回表访问的次数,也就是减少随机IO的发生。
所以把【如果在二级索引的时候,就进行过滤条件判断,不放到回表后判断 】这种情况叫做索引下推
Using where
当我们使用全表扫描来执行对某个表的查询,并且该语句的WHERE子句中有针对该表的搜索条件时
当使用索引访问来执行对某个表的查询,并且该语句的WHERE子句中有除了该索引包含的列之外的其他搜索条件时,在Extra列中也会提示上述额外信息。
Using join buffer (Block Nested Loop)
使用JOIN BUFFER来降低被驱动表的访问次数从而提高查询效率
Not exists
当我们使用左(外)连接时,如果WHERE子句中包含要求被驱动表的某个列等于NULL值的搜索条件,而且那个列又是不允许存储NULL值的,那么在该表的执行计划的Extra列就会提示Not exists额外信息. (也就是如果在被驱动表中匹配到一条记录,那么就不再往下走了)
Using intersect(...)、Using union(...)和Using sort_union(...)
使用到索引合并的时候就会有这种情况啦
Zero limit
当我们的LIMIT子句的参数为0时
Using filesort
不根据所以树进行排序,而在内存中或者磁盘上进行排序的方式统称为文件排序(英文名:filesort)
Using temporary
在许多查询的执行过程中,MySQL可能会借助临时表来完成一些功能,比如去重、排序之类的,比如我们在执行许多包含DISTINCT、GROUP BY、UNION等子句的查询过程中,如果不能有效利用索引来完成查询,MySQL很有可能寻求通过建立内部的临时表来执行查询。
DISTINCT的栗子
Group by 的栗子
Start temporary, End temporary
查询优化器会优先尝试将IN子查询转换成semi-join,而semi-join又有好多种执行策略,当执行策略为DuplicateWeedout时,也就是通过建立临时表来实现为外层查询中的记录进行去重操作时,驱动表查询执行计划的Extra列将显示Start temporary提示,被驱动表查询执行计划的Extra列将显示End temporary提示
LooseScan
在将In子查询转为semi-join时,如果采用的是LooseScan执行策略,则在驱动表执行计划的Extra列就是显示LooseScan提示
FirstMatch(tbl_name)
在将In子查询转为semi-join时,如果采用的是FirstMatch执行策略,则在被驱动表执行计划的Extra列就是显示FirstMatch(tbl_name)提示
Json格式的执行计划
作用
查看某个执行计划花费的成本
用法
在EXPLAIN单词和真正的查询语句中间加上FORMAT=JSON。
EXPLAIN FORMAT=JSON SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key2 WHERE s1.common_field = 'a'
举一个栗子
Extented EXPLAIN
作用
比如可以看到 类似于查询优化器将我们的查询语句重写后的语句
使用
在执行计划执行后, 执行SHOW WARNINGS