1. 准备
- 准备测试数据
为了便于理解,先创建两张测试表,建表及数据写入语句如下:
1 | CREATE DATABASE martin; |
- 尝试explain的用法
下面尝试使用 explain 分析一条 SQL,例子如下:
1 | explain select * from t1 where b=100; |

2. Explain的结果各字段解释
如下:
加粗的列为需要重点关注的项。
| 列名 | 解释 |
|---|---|
| id | 查询编号 |
| select_type | 查询类型:显示本行是简单还是复杂查询 |
| table | 涉及到的表 |
| partitions | 匹配的分区:查询将匹配记录所在的分区。仅当使用 partition 关键字时才显示该列。对于非分区表,该值为 NULL。 |
| type | 本次查询的表连接类型 |
| possible_keys | 可能选择的索引 |
| key | 实际选择的索引 |
| key_len | 被选择的索引长度:一般用于判断联合索引有多少列被选择了 |
| ref | 与索引比较的列 |
| rows | 预计需要扫描的行数,对 InnoDB 来说,这个值是估值,并不一定准确 |
| filtered | 按条件筛选的行的百分比 |
| Extra | 附加信息 |
其中 explain 各列都有各种不同的值,这里介绍几个比较重要列常包含的值:
下面将列出它们常见的一些值,可稍微过一遍,不需要完全记下来,在后续分析SQL时,可以返回查询这篇文章的内容并对比各种值的区别。
3. select_type各种值的解释
| select_type 的值 | 解释 |
|---|---|
| SIMPLE | 简单查询(不使用关联查询或子查询) |
| PRIMARY | 如果包含关联查询或者子查询,则最外层的查询部分标记为primary |
| UNION | 联合查询中第二个及后面的查询 |
| DEPENDENT UNION | 满足依赖外部的关联查询中第二个及以后的查询 |
| UNION RESULT | 联合查询的结果 |
| SUBQUERY | 子查询中的第一个查询 |
| DEPENDENT SUBQUERY | 子查询中的第一个查询,并且依赖外部查询 |
| DERIVED | 用到派生表的查询 |
| MATERIALIZED | 被物化的子查询 |
| UNCACHEABLE SUBQUERY | 一个子查询的结果不能被缓存,必须重新评估外层查询的每一行 |
| UNCACHEABLE UNION | 关联查询第二个或后面的语句属于不可缓存的子查询 |
4. type各种值的解释
| type的值 | 解释 |
|---|---|
| system | 查询对象表只有一行数据,且只能用于 MyISAM 和 Memory 引擎的表,这是最好的情况 |
| const | 基于主键或唯一索引查询,最多返回一条结果 |
| eq_ref | 表连接时基于主键或非 NULL 的唯一索引完成扫描 |
| ref | 基于普通索引的等值查询,或者表间等值连接 |
| fulltext | 全文检索 |
| ref_or_null | 表连接类型是 ref,但进行扫描的索引列中可能包含 NULL 值 |
| index_merge | 利用多个索引 |
| unique_subquery | 子查询中使用唯一索引 |
| index_subquery | 子查询中使用普通索引 |
| range | 利用索引进行范围查询 |
| index | 全索引扫描 |
| ALL | 全表扫描 |
上表的这些情况,查询性能从上到下依次是最好到最差。
5. key_len各种字段类型对应的长度
explain 中的 key_len 列用于表示这次查询中,所选择的索引长度有多少字节,常用于判断联合索引有多少列被选择了。下表总结了常用字段类型的 key_len:
| 列类型 | KEY_LEN | 备注 |
|---|---|---|
| int | key_len = 4+1 | int 为 4 bytes,允许为 NULL,加 1 byte |
| int not null | key_len = 4 | 不允许为 NULL |
| bigint | key_len=8+1 | bigint 为 8 bytes,允许为 NULL 加 1 byte |
| bigint not null | key_len=8 | bigint 为 8 bytes |
| char(30) utf8 | key_len=30*3+1 | char(n)为:n * 3 ,允许为 NULL 加 1 byte |
| char(30) not null utf8 | key_len=30*3 | 不允许为 NULL |
| varchar(30) not null utf8 | key_len=30*3+2 | utf8 每个字符为 3 bytes,变长数据类型,加 2 bytes |
| varchar(30) utf8 | key_len=30*3+2+1 | utf8 每个字符为 3 bytes,允许为 NULL,加 1 byte,变长数据类型,加 2 bytes |
| datetime | key_len=8+1 (MySQL 5.6.4之前的版本);key_len=5+1(MySQL 5.6.4及之后的版本) | 允许为 NULL,加 1 byte |
6. Extra常见值的解释
| Extra 常见的值 | 解释 | 例子 |
|---|---|---|
| Using filesort | 将用外部排序而不是索引排序,数据较小时从内存排序,否则需要在磁盘完成排序 | explain select * from t1 order by create_time; |
| Using temporary | 需要创建一个临时表来存储结构,通常发生对没有索引的列进行 GROUP BY 时 | explain select * from t1 group by create_time; |
| Using index | 使用覆盖索引 | explain select a from t1 where a=111; |
| Using where | 使用 where 语句来处理结果 | explain select * from t1 where create_time=’2019-06-18 14:38:24’; |
| Impossible WHERE | 对 where 子句判断的结果总是 false 而不能选择任何数据 | explain select * from t1 where 1<0; |
| Using join buffer (hash join) | 关联查询中,被驱动表的关联字段没索引 | explain select * from t1 straight_join t2 on (t1.create_time=t2.create_time); |
| Using index condition | 先条件过滤索引,再查数据 | explain select * from t1 where a >900 and a like ‘%9’; |
| Select tables optimized away | 使用某些聚合函数(比如 max、min)来访问存在索引的某个字段是 | explain select max(a) from t1; |
| …… |
7. 对比有无索引的执行计划
条件字段是主键的执行计划
1 | explain select * from t1 where id=100; |

type是const,表示基于主键或唯一索引的查询
key 是PRIMARY,表示走了主键索引
row 表示扫描的行数,只扫描了一行。
条件字段有索引的执行计划
1 | explain select * from t1 where b=100; |

重点关注type,这里是ref,表示:基于普通索引的等值查询,或者表间等值连接
key这个字段,这里可以看出来,是走了索引的。然后再看rows,发现只扫描了1行
条件字段没索引的执行计划
删除b字段上的索引
1 | alter table t1 drop index idx_b; |
再来看刚才这条语句的执行计划
1 | explain select * from t1 where b=100; |

type,是ALL,表示:全表扫描。
key,是NULL,表示没走索引。
rows,这里其实是扫描了很多行,这里是估值,所以不一定准确。
8. 获取分区信息
创建测试表并写入数据
1 | CREATE TABLE sales ( |
查询分区表里面的数据
1 | select * from sales where sale_date='2024-09-17'; |
查看执行计划
1 | explain select * from sales where sale_date='2024-09-17'; |

1 | explain select * from sales where sale_date>'2023-01-01'; |

可以看到,在这种情况下,执行计划就可以显示分区信息。
9. 获取正在执行语句的执行计划
在一个窗口构造一条慢查询
1 | select *,sleep(100) from t1 limit 1; |
在另外一次窗口查看当前连接
1 | show processlist; |

再来查询给定连接的执行计划
1 | EXPLAIN FOR CONNECTION 12; |

就可以看到,这个连接正在执行SQL的执行计划。
这个通常用来分析正在执行的问题SQL。
10. MySQL 8.0执行计划的新用法
树状执行计划
从MySQL 8.0.16开始,可以输出树状执行计划,并且能返回预估成本和预估的返回行数
1 | explain format=tree select * from t1 where a=100; |

cost表示预估成本信息;
rows表示预估扫描行数。
explain analyze
从MySQL 8.0.18开始,引入了EXPLAIN ANALYZE
使用这个,会执行SQL,并返回有关执行成本,返回行数,执行时间,循环次数等信息
1 | explain analyze select * from t1 where a=100; |

cost 表示预估的成本信息
rows 前面的表示预估值,后面的表示实际返回的行数
actual time 第一个值是获取第一行的实际时间,第二个值获取所有行的时间,如果循环了多次就是平均时间,单位毫秒
loops 循环次数