MySQL 中如何看 EXPLAIN 性能分析?一篇讲透执行计划的实战指南
MySQL 中如何看 EXPLAIN 性能分析?一篇讲透执行计划的实战指南
在 MySQL 性能优化里,EXPLAIN 是最常用、也最值得掌握的工具之一。
很多人会用它,但真正拿到结果后,常常会有这些疑问:
type是什么意思?rows大是不是就一定慢?key不为NULL就代表 SQL 没问题了吗?Using filesort、Using temporary到底可不可怕?EXPLAIN、EXPLAIN FORMAT=JSON、EXPLAIN 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 ...;
平时最常用的还是这三个:
EXPLAINEXPLAIN FORMAT=JSONEXPLAIN 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,还要结合rows、key、Extra
3. possible_keys
表示 理论上可能用到的索引。
例如:
possible_keys = idx_status, idx_create_time
这表示优化器认为 idx_status 和 idx_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 BYDISTINCT- 复杂排序
- 派生表 / 子查询处理
它不一定一定有问题,但如果配合大数据量出现,通常值得重点排查。
常见值五:Using join buffer
通常表示 join 没有很好利用索引,需要借助 join buffer。
这往往提示:
- join 条件列缺索引
- 被关联表访问代价较高
六、看 EXPLAIN 的实战顺序
不要一列一列机械看,建议按下面顺序判断。
第一步:先看有没有明显坏信号
优先扫这几个点:
type = ALLkey = NULLrows很大Extra里有Using filesortExtra里有Using temporary
这些如果集中出现,通常就说明 SQL 有明显优化空间。
第二步:看索引是否符合预期
问自己几个问题:
- 我明明建了索引,为什么没用?
- 联合索引是不是只用了第一列?
ORDER BY有没有被索引支持?JOIN条件列有没有索引?
第三步:看扫描行数是否过大
即使 SQL 走了索引,也不一定快。
例如:
type = range
key = idx_create_time
rows = 500000
虽然走了索引,但扫描范围非常大,依然可能很慢。
第四步:看是否有额外排序、临时表
如果 Extra 里有:
Using filesortUsing 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
真实处理行数。
这时你可以和普通 EXPLAIN 的 rows 做对比:
- 预估 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 没问题
这是不够的。
你真正应该综合看的是:
typekeyrowsExtra- 真实执行时的
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,不要只盯着“有没有用索引”,而要优先看这几个字段:
typekeyrowsExtra
其中最危险的一类组合,通常是:
type = ALL
key = NULL
rows 很大
Extra = Using filesort / Using temporary
如果一条 SQL 同时出现这些信号,那它往往就是你应该优先优化的目标。
十六、最后的建议
在实际工作中,建议你按下面顺序使用:
EXPLAIN:先看整体执行计划EXPLAIN FORMAT=JSON:看复杂结构和联合索引细节EXPLAIN ANALYZE:看真实执行耗时和真实行数
这样你分析慢 SQL 时,就不再只是“猜”,而是能基于执行计划一步一步定位问题。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)