适用版本:MySQL 8.0.x
一、为什么 EXPLAIN 这么重要
很多同学在优化 SQL 时,第一反应是:
- “给这个字段建个索引试试”
- “我觉得这里应该走索引”
- “这条 SQL 看起来不复杂,为什么还慢?”
问题在于,“觉得”不等于真实执行路径。
MySQL 在执行一条 SQL 之前,会由优化器评估多种访问方式,选择一条它认为成本更低的路径。EXPLAIN 的价值就在这里:
它能帮助我们看见 MySQL 打算怎么执行这条 SQL。
所以优化 SQL 的一个基本原则是:
- 不要只看 SQL 文本
- 不要只看有没有索引
- 一定要看执行计划
二、EXPLAIN 是什么
EXPLAIN 是 MySQL 用来查看 SQL 执行计划的命令。
最常见的写法:
EXPLAIN SELECT * FROM orders WHERE user_id = 1001;
它不会真正执行查询结果返回,而是告诉你:
- 会访问哪些表
- 可能用哪些索引
- 实际选择了哪个索引
- 大概扫描多少行
- 是否需要额外排序
- 是否会使用临时表
在 MySQL 8.0 中,除了传统 EXPLAIN 外,还常用:
EXPLAIN FORMAT=TRADITIONALEXPLAIN FORMAT=TREEEXPLAIN FORMAT=JSONEXPLAIN ANALYZE
其中 EXPLAIN ANALYZE 非常实用,因为它会真正执行 SQL,并输出更接近真实情况的执行信息。
三、先准备一个示例表
为了后面讲解字段更直观,我们先准备两个表。
CREATE TABLE customers (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
city VARCHAR(50) NOT NULL,
level TINYINT NOT NULL,
created_at DATETIME NOT NULL,
INDEX idx_city_level(city, level)
) ENGINE=InnoDB;
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
customer_id BIGINT NOT NULL,
order_status TINYINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
created_at DATETIME NOT NULL,
INDEX idx_customer_status_time(customer_id, order_status, created_at)
) ENGINE=InnoDB;
有了这两个表,我们可以演示单表查询和关联查询的执行计划。
四、最常见的 EXPLAIN 输出字段
先看一个例子:
EXPLAIN
SELECT *
FROM orders
WHERE customer_id = 1001
AND order_status = 1;
传统格式输出里,最常见的字段有:
idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
下面逐个理解。
五、重点字段逐个讲清楚
1. id
id 表示查询中每个 SELECT 的标识,通常用于判断:
- 子查询执行顺序
- 多表查询的层级关系
- 派生表、联合查询的结构
对于简单单表查询,id 一般就是 1。
2. select_type
表示查询类型,常见值有:
SIMPLE:简单查询,不包含子查询或UNIONPRIMARY:最外层查询SUBQUERY:子查询DERIVED:派生表(通常来自子查询FROM (...))UNION:UNION后面的查询部分
示例:
EXPLAIN
SELECT *
FROM customers
WHERE id IN (
SELECT customer_id FROM orders WHERE order_status = 1
);
这类 SQL 里常会看到 PRIMARY 和 SUBQUERY。
3. table
表示当前这一行执行计划对应的是哪张表,或者哪一个派生表结果。
4. type
这是非常关键的字段,表示访问类型,也可以理解为“查数据的方式”。
常见从好到差可以大致理解为:
systemconsteq_refrefrangeindexALL
const
通常表示通过主键或唯一索引一次就能定位到常量行。
EXPLAIN SELECT * FROM customers WHERE id = 1;
这种访问通常很高效。
eq_ref
常见于关联查询中,表示对前一张表的每一行,都能通过唯一索引在当前表中精确找到一行。
ref
表示使用普通索引或联合索引的等值匹配,通常也不错。
range
表示使用索引做范围扫描,例如:
EXPLAIN
SELECT * FROM orders
WHERE created_at >= '2026-06-01 00:00:00'
AND created_at < '2026-07-01 00:00:00';
index
表示扫描整个索引树,而不是通过条件精准筛选。它通常比全表扫描好一点,但不一定高效。
ALL
表示全表扫描,通常是需要重点关注的信号。
注意:
ALL不等于绝对错误,小表全表扫描有时完全合理。但如果是大表慢 SQL,就要重点排查。
5. possible_keys
表示优化器认为“有可能使用”的索引列表。
注意它只是“候选”,并不代表最终一定会使用。
6. key
表示实际选择使用的索引。
这是最应该重点看的字段之一。
possible_keys不为空,但key为NULL:说明有候选索引,但优化器最终没用key有值:说明确实走了索引
7. key_len
表示 MySQL 决定使用的索引长度。
它很有价值,因为可以帮助你判断:
- 联合索引用了几列
- 是否只用了部分前缀
- 是否因为类型、字符集等导致长度变化
例如索引:
INDEX(customer_id, order_status, created_at)
如果 key_len 只反映前两列长度,说明第三列可能没被真正用于索引匹配。
8. ref
表示索引列和谁进行比较。
常见值包括:
- 常量
- 某个字段
- 函数结果
在关联查询中,这个字段尤其有用,可以看出连接条件怎么参与索引匹配。
9. rows
表示优化器预估需要扫描的行数。
这个字段虽然是估算值,但非常有参考意义。
经验上:
rows越小,通常越好- 如果走了索引但
rows依然很大,说明索引过滤效果有限 - 估算行数和真实行数差异过大时,可能与统计信息有关
10. filtered
表示在当前访问方式下,经过条件过滤后,大概有多少比例的行会保留。
它常与 rows 搭配理解:
rows:先扫多少filtered:扫到的里面大概有多少能通过条件
11. Extra
Extra 很像执行计划的“备注栏”,经常能看出问题所在。
常见值包括:
Using whereUsing indexUsing filesortUsing temporaryUsing index condition
下面单独说。
六、Extra 字段怎么看
1. Using where
表示 MySQL 在存储引擎返回记录后,还需要进一步做条件过滤。
这并不一定是坏事,但说明不是所有条件都在索引层完成。
2. Using index
这是一个很值得关注的信号,通常表示:
- 查询走了覆盖索引
- 所需列都能从索引中直接拿到
- 不需要回表
例如:
EXPLAIN
SELECT customer_id, order_status, created_at
FROM orders
WHERE customer_id = 1001 AND order_status = 1;
如果这些列都包含在联合索引里,就可能看到 Using index。
3. Using filesort
表示 MySQL 需要额外排序,不能直接利用索引顺序完成排序。
这通常值得重点关注,尤其在大结果集场景下。
例如:
EXPLAIN
SELECT *
FROM orders
WHERE customer_id = 1001
ORDER BY amount DESC;
如果没有合适索引支持排序,就可能出现 Using filesort。
4. Using temporary
表示 MySQL 执行过程中用到了临时表,常见于:
GROUP BYDISTINCT- 复杂排序
- 某些子查询和派生表
如果同时看到 Using temporary; Using filesort,往往意味着还有优化空间。
5. Using index condition
表示使用了索引条件下推(ICP,Index Condition Pushdown)等能力,可以减少回表开销。通常是优化信号,但仍需结合整体执行情况判断。
七、单表查询示例分析
示例 1:主键查询
EXPLAIN SELECT * FROM customers WHERE id = 10;
通常特征:
type=constkey=PRIMARYrows=1
这类查询一般非常高效。
示例 2:联合索引等值查询
EXPLAIN
SELECT customer_id, order_status, created_at
FROM orders
WHERE customer_id = 1001
AND order_status = 1;
理想情况:
key=idx_customer_status_timetype=refrows较小Extra=Using index
说明:
- 成功使用联合索引
- 可能形成覆盖索引
示例 3:跳过最左列
EXPLAIN
SELECT * FROM orders
WHERE order_status = 1;
由于联合索引是 (customer_id, order_status, created_at),缺失最左列 customer_id 时,索引利用通常不理想,可能出现:
key=NULLtype=ALL
这时就要回到索引设计本身重新评估。
示例 4:范围查询
EXPLAIN
SELECT *
FROM orders
WHERE customer_id = 1001
AND order_status = 1
AND created_at >= '2026-06-01 00:00:00';
常见情况:
type=rangekey=idx_customer_status_timerows比等值查询多一些
说明联合索引既承担了等值匹配,又承担了范围扫描。
八、多表 JOIN 的执行计划怎么看
优化复杂 SQL 时,很多慢查询问题都出在 JOIN 上。
示例
EXPLAIN
SELECT o.id, o.amount, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.order_status = 1
AND c.city = 'Shanghai';
看 JOIN 时,重点关注:
1. 表的访问顺序
EXPLAIN 输出的每一行,通常可以帮助判断优化器打算先访问哪张表,再访问哪张表。
2. 连接类型是否高效
如果是通过主键或唯一索引做精确匹配,常见较优情况包括:
eq_refref
3. 驱动表与被驱动表
JOIN 中通常会有:
- 先扫描一张表,作为驱动表
- 再根据连接条件去另一张表查匹配记录
如果驱动表返回行数太多,被驱动表又没有合适索引,性能就会明显下降。
4. 关联列是否有索引
以下两类列要重点关注:
JOIN条件中的关联列WHERE中的过滤列
例如这里:
o.customer_idc.ido.order_statusc.city
都值得纳入索引设计考虑。
九、EXPLAIN FORMAT=TREE 与 FORMAT=JSON
MySQL 8.0 中,除了传统表格形式,还支持更丰富的输出方式。
1. FORMAT=TREE
EXPLAIN FORMAT=TREE
SELECT *
FROM orders
WHERE customer_id = 1001 AND order_status = 1;
优点:
- 更接近执行树结构
- 看多层嵌套或 JOIN 更直观
- 容易理解操作层次
2. FORMAT=JSON
EXPLAIN FORMAT=JSON
SELECT *
FROM orders
WHERE customer_id = 1001 AND order_status = 1;
优点:
- 信息更完整
- 字段更细致
- 适合脚本化分析
- 可以查看成本估算等更多细节
如果你是刚入门,建议先以传统格式为主;如果已经开始分析复杂 SQL,TREE 和 JSON 都值得逐步掌握。
十、为什么要学 EXPLAIN ANALYZE
传统 EXPLAIN 展示的是优化器预计怎么执行,但它不真正运行 SQL。
这就意味着:
rows是估算值- 某些判断可能和实际情况有偏差
而 EXPLAIN ANALYZE 会真正执行 SQL,并输出实际执行耗时、循环次数、实际返回行数等信息。
示例:
EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE customer_id = 1001 AND order_status = 1;
它适合解决这类问题:
- 为什么估算只扫 100 行,实际却很慢?
- 为什么看起来走了索引,耗时还是高?
- JOIN 中到底哪个步骤最耗时?
使用时的注意点
- 它会真正执行 SQL,所以在线上大表上要谨慎使用
- 对写操作类语句不要随意执行
- 更适合测试环境、预发环境、只读排查场景
十一、与 EXPLAIN 搭配使用的常用工具
只看 EXPLAIN 还不够,实际排查通常会配合以下工具。
1. SHOW WARNINGS
有时在执行 EXPLAIN 后,可以通过:
SHOW WARNINGS;
查看优化器重写后的 SQL 或更多提示信息。
2. optimizer_trace
如果你想知道“为什么优化器选了 A 而不是 B”,可以开启优化器跟踪:
SET optimizer_trace = 'enabled=on';
执行目标 SQL 后查看:
SELECT * FROM information_schema.OPTIMIZER_TRACE;
它适合分析:
- 候选索引比较
- 成本估算过程
- 查询重写过程
3. SHOW INDEX
执行计划离不开索引结构本身:
SHOW INDEX FROM orders;
确认:
- 索引是否存在
- 联合索引列顺序是否正确
- 是否存在冗余索引
4. 系统表与统计信息
如果 EXPLAIN 的 rows 明显不靠谱,可能与统计信息不准有关。此时可以考虑:
ANALYZE TABLE orders;
让优化器重新采样统计信息。
十二、常见慢 SQL 场景与 EXPLAIN 排查思路
场景 1:明明建了索引,却没用上
排查重点:
possible_keys有什么key是否为NULL- 查询是否有函数计算
- 是否发生隐式类型转换
- 是否跳过联合索引最左列
- 是否因为返回比例太大导致优化器选择全表扫描
场景 2:走了索引还是慢
排查重点:
rows是否过大- 是否发生大量回表
Extra是否有Using filesort- 是否有
Using temporary - 是否本质上返回结果太多
场景 3:ORDER BY 很慢
排查重点:
- 是否出现
Using filesort - 索引顺序是否能兼顾过滤与排序
- 排序字段是否与联合索引顺序匹配
场景 4:GROUP BY 很慢
排查重点:
- 是否出现
Using temporary - 是否能利用索引完成分组
- 是否扫描了太多无关数据
场景 5:JOIN 很慢
排查重点:
- 驱动表返回行数是否太多
- 被驱动表关联列是否有索引
- 连接顺序是否合理
- 是否存在低效的子查询/派生表
十三、一个完整示例:从执行计划看优化方向
假设有 SQL:
SELECT *
FROM orders
WHERE customer_id = 1001
ORDER BY amount DESC
LIMIT 20;
如果执行计划显示:
key=idx_customer_status_timeExtra=Using where; Using filesort
说明什么?
- 过滤阶段可能用到了某个索引
- 但排序列
amount不在可直接利用的索引顺序里 - 所以仍然需要额外排序
优化方向可能包括:
- 是否真的需要按
amount排序 - 是否可以建立更匹配该查询模式的索引
- 是否可以把查询拆成更小范围
- 是否可以先过滤更强,再排序更少数据
再比如:
SELECT customer_id, order_status, created_at
FROM orders
WHERE customer_id = 1001
AND order_status = 1
ORDER BY created_at DESC;
如果索引是:
INDEX(customer_id, order_status, created_at)
那它就更有机会同时兼顾过滤与排序,并且形成覆盖索引。
十四、使用 EXPLAIN 的实战建议
1. 先看 key,再看 type
很多人一上来只看 type,其实更建议先确认:
- 用没用索引
- 用的是哪个索引
- 用得是否符合预期
2. 再看 rows
如果扫描行数太大,即使走索引,也不一定快。
3. 最后重点看 Extra
Using filesort、Using temporary、Using index 往往非常有提示价值。
4. 复杂 SQL 尽量配合 EXPLAIN ANALYZE
传统 EXPLAIN 看的是“估计”,EXPLAIN ANALYZE 更接近“实际”。
5. 执行计划是结果,不是起点
当看到不理想的执行计划时,要继续回到:
- SQL 写法
- 索引设计
- 表结构
- 数据分布
- 返回结果量
不能只盯着某一个字段做表面优化。
十五、小结
这篇文章可以浓缩成几个关键结论:
EXPLAIN是分析 SQL 执行路径的基础工具。- 最重要的字段通常是
key、type、rows、Extra。 Using filesort、Using temporary、ALL往往是慢 SQL 排查的重点信号。- 传统
EXPLAIN看估算,EXPLAIN ANALYZE看更接近真实的执行情况。 - 执行计划必须结合索引设计、SQL 写法和数据分布一起分析。
如果说前两篇文章帮我们建立了“索引为什么有效”的底层认知,那么从这一篇开始,我们就真正进入了调优实践。下一篇我们会把这些知识串起来,专门讲一篇更偏实战的方法论:查询优化实践。
📝 版权声明:本文为原创技术博客,转载请注明出处。
如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!