返回首页

12| 执行计划 EXPLAIN 分析

适用版本: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=TRADITIONAL
  • EXPLAIN FORMAT=TREE
  • EXPLAIN FORMAT=JSON
  • EXPLAIN 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;

传统格式输出里,最常见的字段有:

  • id
  • select_type
  • table
  • partitions
  • type
  • possible_keys
  • key
  • key_len
  • ref
  • rows
  • filtered
  • Extra

下面逐个理解。


五、重点字段逐个讲清楚

1. id

id 表示查询中每个 SELECT 的标识,通常用于判断:

  • 子查询执行顺序
  • 多表查询的层级关系
  • 派生表、联合查询的结构

对于简单单表查询,id 一般就是 1

2. select_type

表示查询类型,常见值有:

  • SIMPLE:简单查询,不包含子查询或 UNION
  • PRIMARY:最外层查询
  • SUBQUERY:子查询
  • DERIVED:派生表(通常来自子查询 FROM (...)
  • UNIONUNION 后面的查询部分

示例:

EXPLAIN
SELECT *
FROM customers
WHERE id IN (
    SELECT customer_id FROM orders WHERE order_status = 1
);

这类 SQL 里常会看到 PRIMARYSUBQUERY

3. table

表示当前这一行执行计划对应的是哪张表,或者哪一个派生表结果。

4. type

这是非常关键的字段,表示访问类型,也可以理解为“查数据的方式”。

常见从好到差可以大致理解为:

  • system
  • const
  • eq_ref
  • ref
  • range
  • index
  • ALL

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 不为空,但 keyNULL:说明有候选索引,但优化器最终没用
  • 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 where
  • Using index
  • Using filesort
  • Using temporary
  • Using 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 BY
  • DISTINCT
  • 复杂排序
  • 某些子查询和派生表

如果同时看到 Using temporary; Using filesort,往往意味着还有优化空间。

5. Using index condition

表示使用了索引条件下推(ICP,Index Condition Pushdown)等能力,可以减少回表开销。通常是优化信号,但仍需结合整体执行情况判断。


七、单表查询示例分析

示例 1:主键查询

EXPLAIN SELECT * FROM customers WHERE id = 10;

通常特征:

  • type=const
  • key=PRIMARY
  • rows=1

这类查询一般非常高效。

示例 2:联合索引等值查询

EXPLAIN
SELECT customer_id, order_status, created_at
FROM orders
WHERE customer_id = 1001
  AND order_status = 1;

理想情况:

  • key=idx_customer_status_time
  • type=ref
  • rows 较小
  • Extra=Using index

说明:

  • 成功使用联合索引
  • 可能形成覆盖索引

示例 3:跳过最左列

EXPLAIN
SELECT * FROM orders
WHERE order_status = 1;

由于联合索引是 (customer_id, order_status, created_at),缺失最左列 customer_id 时,索引利用通常不理想,可能出现:

  • key=NULL
  • type=ALL

这时就要回到索引设计本身重新评估。

示例 4:范围查询

EXPLAIN
SELECT *
FROM orders
WHERE customer_id = 1001
  AND order_status = 1
  AND created_at >= '2026-06-01 00:00:00';

常见情况:

  • type=range
  • key=idx_customer_status_time
  • rows 比等值查询多一些

说明联合索引既承担了等值匹配,又承担了范围扫描。


八、多表 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_ref
  • ref

3. 驱动表与被驱动表

JOIN 中通常会有:

  • 先扫描一张表,作为驱动表
  • 再根据连接条件去另一张表查匹配记录

如果驱动表返回行数太多,被驱动表又没有合适索引,性能就会明显下降。

4. 关联列是否有索引

以下两类列要重点关注:

  • JOIN 条件中的关联列
  • WHERE 中的过滤列

例如这里:

  • o.customer_id
  • c.id
  • o.order_status
  • c.city

都值得纳入索引设计考虑。


九、EXPLAIN FORMAT=TREEFORMAT=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,TREEJSON 都值得逐步掌握。


十、为什么要学 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. 系统表与统计信息

如果 EXPLAINrows 明显不靠谱,可能与统计信息不准有关。此时可以考虑:

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_time
  • Extra=Using where; Using filesort

说明什么?

  1. 过滤阶段可能用到了某个索引
  2. 但排序列 amount 不在可直接利用的索引顺序里
  3. 所以仍然需要额外排序

优化方向可能包括:

  • 是否真的需要按 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 filesortUsing temporaryUsing index 往往非常有提示价值。

4. 复杂 SQL 尽量配合 EXPLAIN ANALYZE

传统 EXPLAIN 看的是“估计”,EXPLAIN ANALYZE 更接近“实际”。

5. 执行计划是结果,不是起点

当看到不理想的执行计划时,要继续回到:

  • SQL 写法
  • 索引设计
  • 表结构
  • 数据分布
  • 返回结果量

不能只盯着某一个字段做表面优化。


十五、小结

这篇文章可以浓缩成几个关键结论:

  1. EXPLAIN 是分析 SQL 执行路径的基础工具。
  2. 最重要的字段通常是 keytyperowsExtra
  3. Using filesortUsing temporaryALL 往往是慢 SQL 排查的重点信号。
  4. 传统 EXPLAIN 看估算,EXPLAIN ANALYZE 看更接近真实的执行情况。
  5. 执行计划必须结合索引设计、SQL 写法和数据分布一起分析。

如果说前两篇文章帮我们建立了“索引为什么有效”的底层认知,那么从这一篇开始,我们就真正进入了调优实践。下一篇我们会把这些知识串起来,专门讲一篇更偏实战的方法论:查询优化实践


📝 版权声明:本文为原创技术博客,转载请注明出处。

如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!

上一篇

11 | B+Tree 与联合索引

下一篇

13|查询优化实践