MySQL 中如何看 EXPLAIN 性能分析?一篇讲透执行计划的实战指南

在 MySQL 性能优化里,EXPLAIN 是最常用、也最值得掌握的工具之一。

很多人会用它,但真正拿到结果后,常常会有这些疑问:

  • type 是什么意思?
  • rows 大是不是就一定慢?
  • key 不为 NULL 就代表 SQL 没问题了吗?
  • Using filesortUsing temporary 到底可不可怕?
  • EXPLAINEXPLAIN FORMAT=JSONEXPLAIN ANALYZE 又有什么区别?

这篇文章会系统讲清楚这些问题,帮助你真正看懂 MySQL 的执行计划,并在工作中用它定位慢 SQL。


一、什么是 EXPLAIN?

EXPLAIN 的作用,是查看 MySQL 优化器打算如何执行这条 SQL

也就是说,它展示的是一份 执行计划(Execution Plan),主要告诉你:

  • 先查哪张表
  • 每张表怎么查
  • 有没有走索引
  • 扫描多少行
  • 是否需要额外排序
  • 是否需要临时表
  • 多表关联是怎么做的

最基础的写法如下:

EXPLAIN
SELECT *
FROM orders
WHERE user_id = 1001;

二、要先搞明白:EXPLAIN 看的是“计划”,不是“最终真实执行结果”

这是很多人最容易混淆的地方。

1. 普通 EXPLAIN:看的是预估计划

普通 EXPLAIN 展示的是优化器的 预估结果,例如:

  • 预估扫描多少行
  • 预估选择哪个索引
  • 预估哪种 join 更优

注意:这是“预计”,不一定和真实执行完全一致。


2. EXPLAIN ANALYZE:看的是实际执行情况

EXPLAIN ANALYZE
SELECT ...

它和普通 EXPLAIN 最大的区别在于:

  • 普通 EXPLAIN:只看计划
  • EXPLAIN ANALYZE会真的执行 SQL,并返回每一步的真实耗时、真实行数、循环次数等信息

所以在排查“为什么看起来走索引了,但还是慢”时,EXPLAIN ANALYZE 特别有价值。


3. EXPLAIN FORMAT=JSON:看更完整的结构化信息

EXPLAIN FORMAT=JSON
SELECT ...

相比传统表格格式,JSON 输出能提供更完整、更层次化的执行计划信息,适合分析复杂 SQL、联合索引使用情况、子查询等场景。


三、最常见的几种写法

EXPLAIN SELECT ...;

EXPLAIN FORMAT=TRADITIONAL SELECT ...;

EXPLAIN FORMAT=JSON SELECT ...;

EXPLAIN FORMAT=TREE SELECT ...;

EXPLAIN ANALYZE SELECT ...;

平时最常用的还是这三个:

  • EXPLAIN
  • EXPLAIN FORMAT=JSON
  • EXPLAIN ANALYZE

四、EXPLAIN 输出字段怎么读?

传统表格格式通常会看到这些列:

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

下面按最实用的角度来讲。


五、最关键的字段详解

1. table

表示当前这一步访问的是哪张表。

如果 SQL 涉及多表关联,通常会出现多行,每一行表示执行计划中的一个步骤。


2. type

type非常重要 的一个字段,表示当前表的访问方式,也可以理解为“查询好不好”。

常见从差到好,大致如下:

  • ALL:全表扫描
  • index:全索引扫描
  • range:索引范围扫描
  • ref:普通索引等值查找
  • eq_ref:唯一索引 / 主键等值关联
  • const:常量级查询
  • system:系统表,极少见

如何理解?

ALL

表示全表扫描,通常说明:

  • 没有可用索引
  • 或者有索引,但优化器认为没必要使用
  • 或者 SQL 写法导致索引失效

如果表很大,ALL 往往就是危险信号。


range

表示范围扫描,例如:

WHERE create_time >= '2026-01-01'

这类条件通常会让索引以范围方式使用。


ref

表示通过非唯一索引做等值查找。

例如:

WHERE status = 1

如果 status 上有普通索引,常见就是 ref


eq_ref

这是 join 场景下比较理想的一种类型,通常出现在:

  • 通过主键关联
  • 通过唯一索引关联

例如:

SELECT *
FROM orders o
JOIN users u ON o.user_id = u.id;

如果 u.id 是主键,那么 users 这一层很可能是 eq_ref


实战建议

看到 type 时,先这么判断:

  • ALL:先警觉
  • range/ref/eq_ref/const:一般比 ALL
  • 但不要只看 type,还要结合 rowskeyExtra

3. possible_keys

表示 理论上可能用到的索引

例如:

possible_keys = idx_status, idx_create_time

这表示优化器认为 idx_statusidx_create_time 都可能可用。

注意,这只是候选索引,不代表最终一定使用。


4. key

表示 实际使用的索引

这个字段极其关键。

两种典型情况

情况一:possible_keys 有值,key 也有值

说明最终确实使用了某个索引。

例如:

possible_keys = idx_status
key = idx_status

情况二:possible_keys 有值,但 key = NULL

说明虽然理论上有索引可用,但优化器最终没有用。

常见原因包括:

  • SQL 写法导致索引失效
  • 扫描范围太大,优化器觉得全表扫更划算
  • 统计信息不准
  • 条件选择性太差

5. key_len

表示本次查询使用的索引长度。

这个字段常用于判断:

  • 联合索引用到了几列
  • 是否只使用了联合索引的前缀部分

例如索引:

KEY idx_a_b_c (a, b, c)

如果 SQL 是:

WHERE a = 1 AND b = 2

key_len 会比只用 a 时更长。

实战中怎么用?

不需要死记每个字段具体占几个字节,你主要看:

  • 同一个索引下,key_len 是否符合预期
  • 是否用到了联合索引的前几列

6. ref

表示索引列和谁进行比较。

常见值有:

  • const:和常量比较
  • 某个列名:和其他表的列比较

例如:

SELECT *
FROM orders o
JOIN users u ON o.user_id = u.id;

users 通过主键匹配,可能看到:

ref = o.user_id

说明是拿 o.user_id 去匹配 u.id


7. rows

这是最值得重点看的字段之一。

它表示 优化器预估当前步骤需要扫描多少行

例如:

rows = 100000

表示 MySQL 估计这一层需要看 10 万行数据。

怎么判断?

  • rows 越大,通常代价越高
  • rows 只是估算值,不一定等于实际值
  • 多表 join 时,要结合整条执行计划一起看

实战经验

如果你本来只想查一小部分数据,但 rows 却非常大,就要重点检查:

  • 索引是否没用上
  • 条件是否写得不合理
  • 统计信息是否失真

8. filtered

表示经过当前条件过滤后,预计还有多少百分比的行会继续参与后续步骤。

例如:

rows = 10000
filtered = 10.00

可以粗略理解为:

  • 扫描了 10000 行
  • 最终约有 10% 继续参与后续处理
  • 也就是大约 1000 行

常见估算公式:

rows × filtered / 100

9. Extra

这是经验上最有价值的一列之一。

它提供额外信息,很多性能问题都可以从这里看出来。

常见值一:Using where

表示 MySQL 在取到记录后,还要继续根据 WHERE 条件过滤。

它不一定是坏事,很多正常 SQL 都会出现。


常见值二:Using index

表示发生了 覆盖索引

也就是说,查询需要的列都能直接从索引中拿到,不需要回表。

这通常是好信号。

例如:

SELECT user_id, status
FROM orders
WHERE user_id = 1;

如果索引正好是:

KEY idx_user_status (user_id, status)

就可能出现 Using index


常见值三:Using filesort

表示 MySQL 需要进行额外排序。

注意,它不一定真的“用文件排序”,这是 MySQL 的术语。它真正想表达的是:

无法直接利用索引顺序完成排序,需要额外做排序操作

如果数据量大,这通常是需要重点关注的信号。

例如:

SELECT *
FROM orders
WHERE status = 1
ORDER BY create_time DESC;

如果缺少合适索引,可能看到:

Using where; Using filesort

常见值四:Using temporary

表示执行过程中使用了临时表。

常见于:

  • GROUP BY
  • DISTINCT
  • 复杂排序
  • 派生表 / 子查询处理

它不一定一定有问题,但如果配合大数据量出现,通常值得重点排查。


常见值五:Using join buffer

通常表示 join 没有很好利用索引,需要借助 join buffer。

这往往提示:

  • join 条件列缺索引
  • 被关联表访问代价较高

六、看 EXPLAIN 的实战顺序

不要一列一列机械看,建议按下面顺序判断。

第一步:先看有没有明显坏信号

优先扫这几个点:

  • type = ALL
  • key = NULL
  • rows 很大
  • Extra 里有 Using filesort
  • Extra 里有 Using temporary

这些如果集中出现,通常就说明 SQL 有明显优化空间。


第二步:看索引是否符合预期

问自己几个问题:

  • 我明明建了索引,为什么没用?
  • 联合索引是不是只用了第一列?
  • ORDER BY 有没有被索引支持?
  • JOIN 条件列有没有索引?

第三步:看扫描行数是否过大

即使 SQL 走了索引,也不一定快。

例如:

type = range
key = idx_create_time
rows = 500000

虽然走了索引,但扫描范围非常大,依然可能很慢。


第四步:看是否有额外排序、临时表

如果 Extra 里有:

  • Using filesort
  • Using temporary

再结合大数据量,往往就是性能瓶颈之一。


七、几个典型案例

案例 1:没索引导致全表扫描

EXPLAIN
SELECT *
FROM user
WHERE phone = '13800138000';

结果可能类似:

type = ALL
key = NULL
rows = 800000
Extra = Using where

解释

  • ALL:全表扫描
  • key = NULL:没使用索引
  • rows = 800000:估计要扫 80 万行

优化思路

phone 建索引:

ALTER TABLE user ADD INDEX idx_phone(phone);

案例 2:走了索引,但仍然可能慢

EXPLAIN
SELECT *
FROM orders
WHERE create_time >= '2026-01-01';

可能结果:

type = range
key = idx_create_time
rows = 300000

解释

  • 的确走了索引
  • 但扫描范围仍然很大

这说明一个重要原则:

走了索引,不等于 SQL 就快。


案例 3:排序没有利用索引

EXPLAIN
SELECT id, user_id, create_time
FROM orders
WHERE status = 1
ORDER BY create_time DESC;

可能看到:

type = ref
key = idx_status
Extra = Using where; Using filesort

解释

  • status 条件用上了索引
  • 但排序仍然无法复用索引顺序
  • 所以要额外排序

优化思路

考虑建立联合索引:

(status, create_time)

案例 4:join 列没索引

EXPLAIN
SELECT *
FROM orders o
JOIN order_detail d ON o.id = d.order_id
WHERE o.id = 100;

如果 d.order_id 没索引,常见现象包括:

  • d 表访问方式较差
  • rows 很大
  • 可能出现 Using join buffer

优化思路

order_detail.order_id 建索引。


八、EXPLAIN FORMAT=JSON 适合看什么?

当普通表格版信息不够用时,可以看:

EXPLAIN FORMAT=JSON
SELECT ...

它更适合分析这些问题:

  • 联合索引到底用了哪些列
  • 子查询 / 派生表的执行层级
  • 条件在哪一层被应用
  • 成本估算更细节的内容

尤其是 used_key_parts 这类字段,比传统输出更直观。

什么时候建议用 JSON?

  • SQL 比较复杂
  • 有子查询、派生表
  • 想确认联合索引用了哪几列
  • 想看更细的成本和过滤信息

九、EXPLAIN ANALYZE 应该怎么看?

EXPLAIN ANALYZE
SELECT ...

重点看这三个维度:

1. actual time

每个节点真实耗时。

如果某一层耗时明显高,那它就是热点。


2. rows

真实处理行数。

这时你可以和普通 EXPLAINrows 做对比:

  • 预估 100 行,实际 10 万行:说明优化器估算偏差很大
  • 预估和实际接近:说明统计信息较靠谱

3. loops

执行循环次数。

在嵌套循环 join 里,如果外层返回很多行,而内层每次都执行,那么 loops 往往会很高,容易暴露 join 性能瓶颈。


十、实际工作中的慢 SQL 排查流程

给你一个很实用的操作顺序。

第 1 步:先跑 EXPLAIN

EXPLAIN SELECT ...;

先快速判断:

  • 哪张表最耗
  • 有没有全表扫描
  • 有没有没走索引
  • 有没有 filesort / temporary

第 2 步:锁定最可疑的步骤

优先看:

  • rows 最大的那一行
  • type 最差的那一行
  • key = NULL 的那一行

第 3 步:检查索引设计

重点检查:

  • WHERE 条件列有没有索引
  • JOIN 列有没有索引
  • ORDER BY / GROUP BY 能否复用索引
  • 联合索引顺序是否合理

第 4 步:必要时看 JSON

EXPLAIN FORMAT=JSON SELECT ...;

确认:

  • 联合索引用了几列
  • 条件附着在哪一层
  • 执行层级是否合理

第 5 步:最后用 EXPLAIN ANALYZE 看真实表现

EXPLAIN ANALYZE SELECT ...;

确认:

  • 真正慢在哪一层
  • 预估行数和实际行数差多少
  • 是否存在统计信息失真

十一、常见导致索引失效、执行计划变差的原因

1. 在索引列上做函数或计算

例如:

WHERE DATE(create_time) = '2026-04-04'

这类写法常常会导致索引无法高效使用。

更推荐改写为范围查询:

WHERE create_time >= '2026-04-04 00:00:00'
  AND create_time <  '2026-04-05 00:00:00'

2. 隐式类型转换

例如字段 phone 是字符串类型,但你写成:

WHERE phone = 13800138000

有时会影响索引使用效果。


3. 联合索引没有按最左前缀使用

索引是:

(a, b, c)

但 SQL 却是:

WHERE b = 1 AND c = 2

这种情况下,通常无法很好利用该联合索引。


4. 前导模糊查询

WHERE name LIKE '%abc'

前面带 %,普通 B-Tree 索引通常难以有效利用。


5. 查询列太多,导致回表成本高

例如:

SELECT *

即使 WHERE 条件走了索引,如果回表次数很多,也可能不够快。


6. 过滤和排序没有一起被索引支持

一个索引用于筛选,另一个逻辑又要求排序,常见结果就是:

  • 条件走了索引
  • 排序仍然 Using filesort

十二、一个非常重要的误区:不要只看“有没有走索引”

很多人分析执行计划时,只看这一点:

  • key 不为 NULL,就觉得 SQL 没问题

这是不够的。

你真正应该综合看的是:

  • type
  • key
  • rows
  • Extra
  • 真实执行时的 actual rows / actual time

也就是说:

“走了索引”只是起点,不是终点。


十三、送你一套简单记忆法

以后看 EXPLAIN,先这样记:

先看三件事

  • 有没有 ALL
  • 有没有 key = NULL
  • rows 大不大

再看两件事

  • 有没有 Using filesort
  • 有没有 Using temporary

最后再细看

  • 联合索引用了几列
  • join 列索引是否合理
  • 预估行数和实际行数差多少

十四、最推荐的日常使用方式

日常初查

EXPLAIN SELECT ...;

分析复杂计划

EXPLAIN FORMAT=JSON SELECT ...;

看真实执行耗时

EXPLAIN ANALYZE SELECT ...;

十五、结尾总结

看 MySQL 的 EXPLAIN,不要只盯着“有没有用索引”,而要优先看这几个字段:

  • type
  • key
  • rows
  • Extra

其中最危险的一类组合,通常是:

type = ALL
key = NULL
rows 很大
Extra = Using filesort / Using temporary

如果一条 SQL 同时出现这些信号,那它往往就是你应该优先优化的目标。


十六、最后的建议

在实际工作中,建议你按下面顺序使用:

  1. EXPLAIN:先看整体执行计划
  2. EXPLAIN FORMAT=JSON:看复杂结构和联合索引细节
  3. EXPLAIN ANALYZE:看真实执行耗时和真实行数

这样你分析慢 SQL 时,就不再只是“猜”,而是能基于执行计划一步一步定位问题。


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐