这是2022年的第一篇文章,新年新征程,祝大家心想事成、健康暴富。接上文MySQL之执行计划详解(一),我们继续学习解读执行计划。执行计划的输出列中,就剩下Extra。今天就来学习一下Extra输出内容的详细含义。

执行计划输出各列的含义详解


Extra
No tables used:
当查询语句没有FROM子句时会提示该额外信息。
DESC SELECT 1;
复制
Impossible WHERE:
WHERE子句永远为FALSE时将会提示该额外信息。
EXPLAIN SELECT * FROM t1 WHERE 1 != 1;
复制
No matching min/max row:
当查询列表处有MIN或者MAX聚集函数,但是并没有符合WHERE子句中的搜索条件的记录时,将会提示该额外信息。
EXPLAIN SELECT MIN(key1) FROM t1 WHERE key1 = 'abcdefg';
复制
Using index:
当查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用索引覆盖的情况下,在Extra列将会提示该额外信息。
EXPLAIN SELECT key1 FROM t1 WHERE key1 = 'a';
复制
Using index condition:
有些搜索条件中虽然出现了索引列,但却不能使用到索引:
DESC SELECT * FROM t1 WHERE key1 > 'z' AND key1 LIKE '%a';
复制
其中的key1 > 'z'可以使用到索引,但是key1 LIKE '%a'却无法使用到索引,在以前版本的MySQL中,是按照下边步骤来执行这个查询的:
1、先根据key1 > 'z'这个条件,从二级索引idx_key1中获取到对应的二级索引记录。
2、根据上一步骤得到的二级索引记录中的主键值进行回表,找到完整的用户记录再检测该记录是否符合key1 LIKE '%a'这个条件,将符合条件的记录加入到最后的结果集。
但是虽然key1 LIKE '%a'不能组成范围区间参与range访问方法的执行,但这个条件毕竟只涉及到了key1列,所以MySQL将上面的步骤改进了一下:
1、先根据key1 > 'z'这个条件,定位到二级索引idx_key1中对应的二级索引记录。
2、对于指定的二级索引记录,先不着急回表,而是先检测一下该记录是否满足key1 LIKE '%a'这个条件,如果这个条件不满足,则该二级索引记录没必要回表。
3、对于满足key1 LIKE '%a'这个条件的二级索引记录执行回表操作。
回表操作其实是一个随机IO,比较耗时,所以上述修改虽然只改进了一点点,但是可以省去好多回表操作的成本。MySQL把这个改进称之为索引条件下推(英文名:Index Condition Pushdown)。
如果在查询语句的执行过程中将要使用索引条件下推这个特性,在Extra列中将会显示Using index condition。
DESC SELECT * FROM t1 WHERE key1 > 'z' AND key1 LIKE '%b';
复制
Using where:
当我们使用全表扫描来执行对某个表的查询,并且该语句的WHERE子句中有针对该表的搜索条件时,在Extra列中会提示上述额外信息。
DESC SELECT * FROM t1 WHERE common_field = 'a';
复制
当使用索引访问来执行对某个表的查询,并且该语句的WHERE子句中有除了该索引包含的列之外的其他搜索条件时,在Extra列中也会提示上述额外信息。比如下边这个查询虽然使用idx_key1索引执行查询,但是搜索条件中除了包含key1的搜索条件key1 = 'a',还有包含common_field的搜索条件,所以Extra列会显示Using where的提示:
DESC SELECT * FROM t1 WHERE key1 = 'a' AND common_field = 'a';
复制
Using join buffer (Block Nested Loop):
在连接查询执行过程中,当被驱动表不能有效的利用索引加快访问速度,MySQL一般会为其分配一块名叫join buffer的内存块来加快查询速度,也就是我们所讲的基于块的嵌套循环算法(这个只是大家应该很熟悉了,哈哈,这回就把知识串起来了)。
DESC SELECT * FROM t1 INNER JOIN t2 ON t1.common_field = t2.common_field;
复制
对t2表的执行计划的Extra列显示了两个提示:
Using join buffer (Block Nested Loop):这是因为对表t2的访问不能有效利用索引,只好退而求其次,使用join buffer来减少对t2表的访问次数,从而提高性能。
Using where:可以看到查询语句中有一个t1.common_field = t2.common_field条件,因为t1是驱动表,t2是被驱动表,所以在访问t2表时,t1.common_field的值已经确定下来了,所以实际上查询t2表的条件就是t2.common_field = 一个常数,所以提示了Using where额外信息。
Not exists:
使用左(外)连接时,如果WHERE子句中包含要求被驱动表的某个列等于NULL值的搜索条件,而且那个列又是不允许存储NULL值的,那么在该表的执行计划的Extra列就会提示Not exists额外信息。
DESC SELECT * FROM t1 LEFT JOIN t2 ON t1.key1 = t2.key1 WHERE t2.id IS NULL;
复制
上述查询中t1表是驱动表,t2表是被驱动表,t2.id列是不允许存储NULL值的,而WHERE子句中又包含t2.id IS NULL的搜索条件,这意味着必定是驱动表的记录在被驱动表中找不到匹配ON子句条件的记录才会把该驱动表的记录加入到最终的结果集,所以对于某条驱动表中的记录来说,如果能在被驱动表中找到1条符合ON子句条件的记录,那么该驱动表的记录就不会被加入到最终的结果集,也就是说我们没有必要到被驱动表中找到全部符合ON子句条件的记录,这样可以稍微节省一点性能。
Using intersect(...)、Using union(...)和Using sort_union(...):
如果执行计划的Extra列出现了Using intersect(...)提示,说明准备使用Intersect索引合并的方式执行查询,括号中的...表示需要进行索引合并的索引名称;如果出现了Using union(...)提示,说明准备使用Union索引合并的方式执行查询;出现了Using sort_union(...)提示,说明准备使用Sort-Union索引合并的方式执行查询。
DESC SELECT * FROM t1 WHERE key1 = '61' AND key3 = 'f05';
复制
Extra列显示了Using intersect(idx_key3,idx_key1),表明MySQL即将使用idx_key3和idx_key1这两个索引进行Intersect索引合并的方式执行查询。
Zero limit:
LIMIT子句的参数为0时,表示不打算从表中读出任何记录,将会提示该额外信息。
EXPLAIN SELECT * FROM t1 LIMIT 0;
复制
Using filesort:
有一些情况下对结果集中的记录进行排序是可以使用到索引的。
DESC SELECT * FROM t1 ORDER BY key1 LIMIT 10;
复制
这个查询语句可以利用idx_key1索引直接取出key1列的10条记录,然后再进行回表操作就好了。但是很多情况下排序操作无法使用到索引,只能在内存中(记录较少的时候)或者磁盘中(记录较多的时候)进行排序,MySQL把这种在内存中或者磁盘上进行排序的方式统称为文件排序(英文名:filesort)。如果某个查询需要使用文件排序的方式执行查询,就会在执行计划的Extra列中显示Using filesort提示。
DESC SELECT * FROM t1 ORDER BY common_field LIMIT 10;
复制
Using temporary:
在许多查询的执行过程中,MySQL可能会借助临时表来完成一些功能,比如去重、排序之类的,比如我们在执行许多包含DISTINCT、GROUP BY、UNION等子句的查询过程中,如果不能有效利用索引来完成查询,MySQL很有可能寻求通过建立内部的临时表来执行查询。如果查询中使用到了内部的临时表,在执行计划的Extra列将会显示Using temporary提示。
DESC SELECT DISTINCT common_field FROM t1;
复制
DESC SELECT common_field, COUNT(*) AS amount FROM t1 GROUP BY common_field;
复制
DESC SELECT common_field, COUNT(*) AS amount FROM t1 GROUP BY common_field ORDER BY common_field;
复制
DESC SELECT common_field, COUNT(*) AS amount FROM t1 GROUP BY common_field ORDER BY NULL;
复制
另外,执行计划中出现Using temporary并不是一个好的征兆,因为建立与维护临时表要付出很大成本的,所以我们最好能使用索引来替代掉使用临时表,比方说下边这个包含GROUP BY子句的查询就不需要使用临时表:
DESC SELECT key1, COUNT(*) AS amount FROM t1 GROUP BY key1;
复制
Start temporary, End temporary:
LooseScan:
DESC SELECT * FROM t1 WHERE key3 IN (SELECT key1 FROM t2 WHERE key1 > 'z');
复制
FirstMatch(table_name):
DESC SELECT * FROM t1 WHERE common_field IN (SELECT key1 FROM t2 where t1.key3 = t2.key3);
复制

其他格式的执行计划


JSON格式的执行计划
EXPLAIN语句输出中缺少了一个衡量执行计划好坏的重要属性 —— 成本。不过MySQL贴心的为我们提供了一种查看某个执行计划花费的成本的方式:
在EXPLAIN(DESC或DESCRIBE)
和真正的查询语句中间加上FORMAT=JSON
。这样我们就可以得到一个json格式的执行计划,里边儿包含该计划花费的成本。
mysql> DESC FORMAT=JSON SELECT * FROM t1 INNER JOIN t2 ON t1.key1 = t2.key2 WHERE t1.common_field = 'a'\G*************************** 1. row ***************************EXPLAIN: { "query_block": { "select_id": 1, "cost_info": { "query_cost": "3294.12" }, "nested_loop": [ { "table": { "table_name": "t1", "access_type": "ALL", "possible_keys": [ "idx_key1" ], "rows_examined_per_scan": 9991, "rows_produced_per_join": 999, "filtered": "10.00", "cost_info": { "read_cost": "1895.38", "eval_cost": "199.82", "prefix_cost": "2095.20", "data_read_per_join": "2M" }, "used_columns": [ "id", "key1", "key2", "key3", "key_part1", "key_part2", "key_part3", "common_field" ], "attached_condition": "((`test_cost`.`t1`.`common_field` = 'a') and (`test_cost`.`t1`.`key1` is not null))" } }, { "table": { "table_name": "t2", "access_type": "ref", "possible_keys": [ "uq_key2" ], "key": "uq_key2", "used_key_parts": [ "key2" ], "key_length": "5", "ref": [ "test_cost.t1.key1" ], "rows_examined_per_scan": 1, "rows_produced_per_join": 999, "filtered": "100.00", "index_condition": "(`test_cost`.`t1`.`key1` = `test_cost`.`t2`.`key2`)", "cost_info": { "read_cost": "999.10", "eval_cost": "199.82", "prefix_cost": "3294.12", "data_read_per_join": "2M" }, "used_columns": [ "id", "key1", "key2", "key3", "key_part1", "key_part2", "key_part3", "common_field" ] } } ] }}1 row in set, 2 warnings (0.01 sec)
复制
输出的内容有些多,我们来一起看一下:
输出的内容 | 描述 |
---|---|
整个查询语句只有1个SELECT关键字,该关键字对应的id号为1 | |
整个查询的执行成本预计为3294.12 | |
几个表之间采用嵌套循环连接算法执行,以下是参与嵌套循环连接算法的各个表的信息 | |
t1表是驱动表 访问方法为ALL,意味着使用全表扫描访问 可能使用的索引:idx_key1 | |
查询一次t1表大致需要扫描9991条记录 驱动表t1的扇出是999 condition filtering代表的百分比:10% | |
单次查询t1表总共的成本为2095.20 读取的数据量2M(2×1024×1024) | |
![]() | 执行查询中涉及到的列 |
对t1表访问时针对单表查询的条件:"((`test_cost`.`t1`.`common_field` = 'a') and (`test_cost`.`t1`.`key1` is not null))" | |
t2表是被驱动表 访问方法为ref,意味着使用索引等值匹配的方式访问 可能使用的索引:uq_key2 使用到的索引列:key2 key_len:5 与key2列进行等值匹配的对象:test_cost.t1.key1 | |
查询一次t2表大致需要扫描1条记录 被驱动表t2的扇出是999(由于后边没有多余的表进行连接,所以这个值也没啥用) condition filtering代表的百分比:100% | |
t2表使用索引进行查询的搜索条件:"(`test_cost`.`t1`.`key1` = `test_cost`.`t2`.`key2`)" | |
单次查询t1、多次查询t2表总共的成本3294.12 读取的数据量2M(2×1024×1024) | |
执行查询中涉及到的列 |
上述的输出中,我们有一个部分没有说明。下面先来看一下t1表的"cost_info"部分:
"cost_info": { "read_cost": "1895.38", "eval_cost": "199.82", "prefix_cost": "2095.20", "data_read_per_join": "2M"}
复制
read_cost的组成:
eval_cost的组成:
prefix_cost就是单独查询t1表的成本:
data_read_per_join:
再来看下t2表的"cost_info"部分:
"cost_info": { "read_cost": "999.10", "eval_cost": "199.82", "prefix_cost": "3294.12", "data_read_per_join": "2M"}
复制
由于t2表是被驱动表,所以可能被读取多次,这里的read_cost和eval_cost是访问多次t2表后累加起来的值,主要关注prefix_cost的值代表的是整个连接查询预计的成本,也就是单次查询t1表和多次查询t2表后的成本的和,也就是:
prefix_cost(t1表) + read_cost(t2表) + eval_cost(t2表) = 总成本2095.20 + 999.10 + 199.82 = 3294.12
复制
所以最终求得执行查询的总成本是:3294.12。

TREE格式的执行计划
mysql> DESC FORMAT=TREE SELECT * FROM student t1 INNER JOIN student2 t2 ON t1.address = t2.address WHERE t1.address LIKE '北京市%'\G*************************** 1. row ***************************EXPLAIN: -> Filter: (t2.address = t1.address) (cost=4.95 rows=3) -> Inner hash join (<hash>(t2.address)=<hash>(t1.address)) (cost=4.95 rows=3) -> Table scan on t2 (cost=0.23 rows=16) -> Hash -> Filter: (t1.address like '北京市%') (cost=1.85 rows=2) -> Table scan on t1 (cost=1.85 rows=16)1 row in set (0.00 sec)
复制
TREE格式的执行计划将查询的每一个步骤都分解成树型进行显示,由上可知,执行这个查询的总成本是:4.95。

执行计划扩展信息

我们在使用EXPLAIN(DESC或DESCRIBE)查看某个查询的执行计划后,还可以使用SHOW WARNINGS语句查看与这个查询的执行计划有关的一些扩展信息。
EXPLAIN SELECT t1.key1, t2.key1 FROM t1 LEFT JOIN t2 ON t1.key1 = t2.key1 WHERE t2.common_field IS NOT NULL;SHOW WARNINGS\G
复制
可以看到SHOW WARNINGS展示出来的信息有三个字段,分别是Level、Code、Message。我们最常见的就是Code为1003的信息,当Code值为1003时,Message字段展示的信息类似于查询优化器将我们的查询语句重写后的语句。比如我们上边的查询本来是一个左(外)连接查询,但是有一个t2.common_field IS NOT NULL的条件,就会导致查询优化器把左(外)连接查询优化为内连接查询,从SHOW WARNINGS的Message字段也可以看出来,原本的LEFT JOIN已经变成了JOIN。

小结


参考资料
https://dev.mysql.com/doc/refman/5.7/en/explain-output.html#explain_extra
小孩子4919《MySQL是怎样运行的:从根上理解MySQL》

end