官方文档路径:https://dev.mysql.com/doc/refman/5.7/en/explain-output.html#explain-extra-information
顾名思义, Extra 列是用来说明一些额外信息的, 我们可以通过这些额外信息来更准确的理解 MySQL 到底将如何执行给定的查询语句。 MySQL 提供的额外信息很多, 几十个, 无法一一介绍, 挑一些平时常见的或者比较重要的额外信息讲讲, 同时因为数据的关系, 很难实际演示出来, 所以本小节的相关内容不会提供全部实际的 SQL 语句和结果画面。
No tables used
当查询语句的没有 FROM 子句时将会提示该额外信息。
mysql> explain select 1;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | No tables used |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
1 row in set, 1 warning (0.00 sec)
Impossible WHERE
查询语句的 WHERE 子句永远为 FALSE 时将会提示该额外信息。
mysql> explain select * from s1 where 1 = 0;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | Impossible WHERE |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------+
1 row in set, 1 warning (0.00 sec)
No matching min/max row
当查询列表处有 MIN 或者 MAX 聚集函数, 但是并没有符合 WHERE 子句中的搜索条件的记录时, 将会提示该额外信息。
Using index
当我们的查询列表以及搜索条件中只包含属于某个索引的列, 也就是在可以使用索引覆盖的情况下, 在 Extra 列将会提示该额外信息。 比方说下边这个查询中只需要用到 idx_order_no 而不需要回表操作:
mysql> EXPLAIN SELECT expire_time FROM s1 WHERE insert_time = '2021-03-22 18:36:47';
+----+-------------+-------+------------+------+----------------------------------+------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+----------------------------------+------------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | ref | u_idx_day_status,idx_insert_time | u_idx_day_status | 5 | const | 14 | 100.00 | Using index |
+----+-------------+-------+------------+------+----------------------------------+------------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
Using index condition
有些搜索条件中虽然出现了索引列, 但却不能使用到索引, 比如下边这个查询:
SELECT * FROM s1 WHERE order_no > 'z' AND order_no LIKE '%a';
其中的 order_no> 'z’可以使用到索引, 但是 order_no LIKE '%a’却无法使用到索引, 在以前版本的 MySQL 中, 是按照下边步骤来执行这个查询的:
1、 先根据 order_no> 'z’这个条件, 从二级索引 idx_order_no 中获取到对应的二级索引记录。
2、 根据上一步骤得到的二级索引记录中的主键值进行回表(因为是 select *),找到完整的用户记录再检测该记录是否符合 key1 LIKE '%a’这个条件, 将符合条件的记录加入到最后的结果集。
但是虽然 order_no LIKE '%a’不能组成范围区间参与 range 访问方法的执行,但这个条件毕竟只涉及到了 order_no 列, MySQL 把上边的步骤改进了一下。
索引条件下推
1、 先根据 order_no> 'z’这个条件, 定位到二级索引 idx_order_no 中对应的二级索引记录。
2、 对于指定的二级索引记录, 先不着急回表, 而是先检测一下该记录是否满足 order_no LIKE '%a’这个条件, 如果这个条件不满足, 则该二级索引记录压根儿就没必要回表。
3、 对于满足 order_no LIKE '%a’这个条件的二级索引记录执行回表操作。
我们说回表操作其实是一个随机 IO, 比较耗时, 所以上述修改可以省去很多回表操作的成本。 这个改进称之为索引条件下推(英文名: ICP , Index Condition Pushdown) 。
如果在查询语句的执行过程中将要使用索引条件下推这个特性, 在 Extra 列中将会显示 Using index condition, 比如这样:
mysql> explain SELECT * FROM s1 WHERE order_no > 'z' AND order_no LIKE '%a';
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | s1 | NULL | range | idx_order_no | idx_order_no | 152 | NULL | 1 | 100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)
Using where
当我们使用全表扫描来执行对某个表的查询, 并且该语句的 WHERE 子句中有针对该表的搜索条件时, 在 Extra 列中会提示上述额外信息。
mysql> EXPLAIN SELECT * FROM s1 WHERE order_note = 'a';
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 10692 | 10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
当使用索引访问来执行对某个表的查询, 并且该语句的 WHERE 子句中有除了该索引包含的列之外的其他搜索条件时, 在 Extra 列中也会提示上述信息。
比如下边这个查询虽然使用 idx_order_no 索引执行查询, 但是搜索条件中除了包含 order_no 的搜索条件 order_no = ‘a’, 还有包含 order_note 的搜索条件,此时需要回表检索记录然后进行条件判断, 所以 Extra 列会显示 Using where 的
提示:
mysql> EXPLAIN SELECT * FROM s1 WHERE order_no = 'a' AND order_note = 'a';
+----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | ref | idx_order_no | idx_order_no | 152 | const | 1 | 10.00 | Using where |
+----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
但是大家注意: 出现了 Using where, 只是表示在 server 层根据 where 条件进行了过滤, 和是否全表扫描或读取了索引文件没有关系, 网上有不少文章把Using where 和是否读取索引进行关联, 是不正确的, 也有文章把 Using where 和
回表进行了关联, 这也是不对的。 按照 MySQL 官方的说明:
意思是: Extra 列中出现了 Using where 代表 WHERE 子句用于限制要与下一个表匹配或发送给客户端的行。
很明显, Using where 只是表示 MySQL 使用 where 子句中的条件对记录进行了过滤。
Using join buffer (Block Nested Loop)
在连接查询执行过程中, 当被驱动表不能有效的利用索引加快访问速度,MySQL 一般会为其分配一块名叫 join buffer 的内存块来加快查询速度:
mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.order_note = s2.order_note;
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+----------------------------------------------------+
| 1 | SIMPLE | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 10621 | 100.00 | NULL |
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 10692 | 10.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+----------------------------------------------------+
2 rows in set, 1 warning (0.02 sec)
我们看到, 在对 s1 表的执行计划的 Extra 列显示了两个提示:
Using join buffer (Block Nested Loop): 这是因为对表 s1 的访问不能有效利用索引, 只好退而求其次, 使用 join buffer 来减少对 s1 表的访问次数, 从而提高性能。
Using where: 可以看到查询语句中有一个 s1.order_note = s2.order_note 条件, 因为 s2 是驱动表, s1 是被驱动表, 所以在访问 s1 表时, s1.order_note 的值已经确定下来了, 所以实际上查询 s1 表的条件就是 s1.order_note = 一个常数,所以提示了 Using where 额外信息。
Not exists
当我们使用左(外) 连接时, 如果 WHERE 子句中包含要求被驱动表的某个列等于 NULL 值的搜索条件, 而且那个列又是不允许存储 NULL 值的, 那么在该表的执行计划的 Extra 列就会提示 Not exists 额外信息, 比如这样:
mysql> EXPLAIN SELECT * FROM s1 LEFT JOIN s2 ON s1.order_no = s2.order_no WHERE s2.id IS NULL;
+----+-------------+-------+------------+------+---------------+--------------+---------+------------------+-------+----------+-------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+--------------+---------+------------------+-------+----------+-------------------------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 10692 | 100.00 | NULL |
| 1 | SIMPLE | s2 | NULL | ref | idx_order_no | idx_order_no | 152 | test.s1.order_no | 1 | 10.00 | Using where; Not exists |
+----+-------------+-------+------------+------+---------------+--------------+---------+------------------+-------+----------+-------------------------+
2 rows in set, 1 warning (0.00 sec)
上述查询中 s1 表是驱动表, s2 表是被驱动表, s2.id 列是主键而且不允许存储 NULL 值的, 而 WHERE 子句中又包含 s2.id IS NULL 的搜索条件。
Using intersect(…)、 Using union(…)和 Using sort_union(…)
如果执行计划的 Extra 列出现了 Using intersect(…)提示, 说明准备使用Intersect 索引合并的方式执行查询, 括号中的…表示需要进行索引合并的索引名称; 如果出现了 Using union(…)提示, 说明准备使用 Union 索引合并的方式执行查询; 出现了 Using sort_union(…)提示, 说明准备使用 Sort-Union 索引合并的方式执行查询。 什么是索引合并, 我们后面会单独讲。
Zero limit
当我们的 LIMIT 子句的参数为 0 时, 表示压根儿不打算从表中读出任何记录,将会提示该额外信息。
Using filesort
有一些情况下对结果集中的记录进行排序是可以使用到索引的, 比如下边这个查询:
mysql> EXPLAIN SELECT * FROM s1 ORDER BY order_no LIMIT 10;
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------+
| 1 | SIMPLE | s1 | NULL | index | NULL | idx_order_no | 152 | NULL | 10 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)
这个查询语句可以利用idx_order_no索引直接取出order_no列的10条记录,然后再进行回表操作就好了。 但是很多情况下排序操作无法使用到索引, 只能在内存中(记录较少的时候) 或者磁盘中(记录较多的时候) 进行排序, MySQL把这种在内存中或者磁盘上进行排序的方式统称为文件排序。 如果某个查询需要使用文件排序的方式执行查询, 就会在执行计划的 Extra 列中显示 Using filesort提示:
mysql> EXPLAIN SELECT * FROM s1 ORDER BY order_note LIMIT 10;
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+----------------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 10692 | 100.00 | Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+----------------+
1 row in set, 1 warning (0.00 sec)
需要注意的是, 如果查询中需要使用 filesort 的方式进行排序的记录非常多,那么这个过程是很耗费性能的, 我们最好想办法将使用文件排序的执行方式改为使用索引进行排序。
Using temporary
在许多查询的执行过程中, MySQL 可能会借助临时表来完成一些功能, 比如去重、 排序之类的, 比如我们在执行许多包含 DISTINCT、 GROUP BY、 UNION 等子句的查询过程中, 如果不能有效利用索引来完成查询, MySQL 很有可能寻求通过建立内部的临时表来执行查询。 如果查询中使用到了内部的临时表, 在执行计划的 Extra 列将会显示 Using temporary 提示:
mysql> EXPLAIN SELECT DISTINCT order_note FROM s1;
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-----------------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 10692 | 100.00 | Using temporary |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-----------------+
1 row in set, 1 warning (0.01 sec)
mysql> EXPLAIN SELECT order_note, COUNT(*) AS amount FROM s1 GROUP BY order_note;
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+---------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+---------------------------------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 10692 | 100.00 | Using temporary; Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+---------------------------------+
1 row in set, 1 warning (0.00 sec)
上述执行计划的 Extra 列不仅仅包含 Using temporary 提示, 还包含 Using filesort 提示, 可是我们的查询语句中明明没有写 ORDER BY 子句呀? 这是因为MySQL 会在包含 GROUP BY 子句的查询中默认添加上 ORDER BY 子句, 也就是说
上述查询其实和下边这个查询等价:
EXPLAIN SELECT order_note, COUNT(*) AS amount FROM s1 GROUP BY order_note order by order_note;
如果我们并不想为包含 GROUP BY 子句的查询进行排序, 需要我们显式的写上 ORDER BY NULL:
mysql> EXPLAIN SELECT order_note, COUNT(*) AS amount FROM s1 GROUP BY order_note order by null;
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-----------------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 10692 | 100.00 | Using temporary |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-----------------+
1 row in set, 1 warning (0.00 sec)
这回执行计划中就没有 Using filesort 的提示了, 也就意味着执行查询时可以省去对记录进行文件排序的成本了。
很明显, 执行计划中出现 Using temporary 并不是一个好的征兆, 因为建立与维护临时表要付出很大成本的, 所以我们最好能使用索引来替代掉使用临时表,比方说下边这个包含 GROUP BY 子句的查询就不需要使用临时表:
mysql> EXPLAIN SELECT order_no, COUNT(*) AS amount FROM s1 GROUP BY order_no;
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+-------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+-------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | index | idx_order_no | idx_order_no | 152 | NULL | 10692 | 100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+-------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
从 Extra 的 Using index 的提示里我们可以看出, 上述查询只需要扫描 idx_order_no 索引就可以搞定了, 不再需要临时表了。
总的来说, 发现在执行计划里面有using filesort或者Using temporary的时候,特别需要注意, 这往往存在着很大优化的余地, 最好进行改进, 变为使用 Using index 会更好
Start temporary, End temporary
有子查询时, 查询优化器会优先尝试将 IN 子查询转换成 semi-join(半连接优化技术, 本质上是把子查询上拉到父查询中, 与父查询的表做 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)提示。