MySQL EXPLAIN 详解:别背字段,先看出这条 SQL 慢在哪
EXPLAIN 真正有用的地方,不是把 type、rows、key、Extra 一列列背下来,而是借这几个信号尽快判断:这条 SQL 到底慢在扫描过重、索引路径不对,还是排序和临时处理把成本抬高了。
做 SQL 优化时,EXPLAIN 几乎总会最先被贴出来。
但很多人真正卡住的,不是不会跑 EXPLAIN,而是结果出来以后,还是不知道这条 SQL 到底慢在哪。
于是执行计划经常会被读成两种样子:
- 要么只看见
key不为空,就觉得“用了索引,应该问题不大” - 要么把
type、rows、key、Extra一列列解释得很完整,最后还是不知道该先改什么
我现在更习惯把 EXPLAIN 当成一张事故现场图,而不是字段手册。
它最有价值的地方,不是教你背定义,而是帮你尽快回答这几个更实际的问题:
- 这条 SQL 是在精准找,还是在扫一大片
- 优化器走的索引路径,到底是不是一条便宜的路
- 把成本真正抬高的,是扫描量、排序,还是临时处理
先看一个更接近现场的慢 SQL
比如订单列表页有这样一条查询:
EXPLAIN
select id, order_no, amount, create_time
from orders
where status = 'PAID'
order by create_time desc
limit 20;
页面上看到的现象是:
- 数据量上来以后,这个列表接口越来越慢
- 明明只取 20 条,RT 却很不稳定
- 研发第一眼看到
status上有索引,觉得不至于太差
这时 EXPLAIN 可能长这样:
type: range
possible_keys: idx_status, idx_create_time
key: idx_status
rows: 180000
Extra: Using where; Using filesort
如果你按字段讲义的方式读,当然也能说:
type是rangekey用了idx_statusrows预估扫描 18 万行Extra里有Using filesort
但真实排查时,更重要的是把它尽快翻译成一句人话:
- MySQL 先沿着
status把已支付订单捞出来 - 这个候选集还是很大
- 排序又没法顺着索引直接完成
- 所以后面还要对一大批候选结果额外排序
到这一步,这条 SQL 的慢点其实已经很具体了:
不是“有没有索引”这么简单,而是先按一个区分度不够高的条件扫出很多数据,再做额外排序,结果只要 20 条,前面却已经干了不少活。
这就是我现在最在意的 EXPLAIN 读法:不是把每列都讲清楚,而是尽快坐实数据库到底准备怎么干活。
我通常只先看这四个信号
真在排一条慢 SQL 时,我一般不会从左到右把整张执行计划读完,而是先盯这四个点:
typerowskeyExtra
因为大多数时候,慢点是不是已经很明显,就看这四个信号够不够把现场收住。
先看 type:这是精准找,还是已经扫开了
type 最重要的作用,不是让你记排名,而是先建立一个直觉:
- 数据库这次是在精确定位
- 还是已经退到比较粗的访问方式
像 const、eq_ref、ref,通常更像“找得比较准”;range 则要结合场景看;如果已经看到 index 或 ALL,就该马上提高警惕。
但 type 不能单看。
比如这条 SQL 里 range 不算最差,可它也绝不代表没问题。因为下一眼再看 rows,你就会知道这次扫描其实并不轻。
再看 rows:为了拿 20 条,前面准备扫多少
我看执行计划时,经常会把 rows 看得比“用了没用索引”还重。
因为很多 SQL 慢,不是完全没走索引,而是:
- 虽然走了索引
- 但这个索引过滤能力不够
- 数据库还是得先扫出一大批候选结果
像这里的 rows = 180000,就已经很说明问题了。
最终只返回 20 条,但前面准备处理 18 万行候选数据,这条路径显然谈不上便宜。
所以 rows 真正告诉你的,不只是一个估算值,而是:
为了拿到这点结果,数据库前面到底付了多大代价。
第三眼看 key:它到底走了哪条索引路
很多人看到 key 不为空,就容易先放心。
但这里真正该问的是:
- 为什么走的是
idx_status - 这条路为什么会把候选集放得这么大
- 有没有机会让过滤和排序走到同一条索引路径里
回到这条 SQL,key = idx_status 只能说明一件事:
- 优化器先用了状态索引去找数据
但 status = 'PAID' 这种条件,在订单表里往往区分度并不高。也就是说,“用了索引”这件事本身,并不代表扫描就轻。
如果顺着这个判断继续往下想,后面更值得关心的就不是字段定义,而是:
- 能不能改成更贴近
where + order by的联合索引 - 能不能先把候选集收得更小
- 这条列表查询有没有同时叠加深分页或回表成本
最后看 Extra:真正把成本抬起来的那一下
很多 SQL 真正拖慢响应,不是因为完全没索引,而是找到候选数据以后,后面还要再做额外处理。
这里最值得先盯的,就是 Extra。
在这个例子里,最扎眼的是:
Using filesort
它至少说明一件事:
- MySQL 没法顺着当前索引顺序直接把结果吐出来
- 还要额外做一次排序
如果候选集本来就已经很大,这一步的成本就会被明显放大。
所以把前面几个信号合起来,这条 SQL 的判断线其实很顺:
type = range,说明不是精准命中,而是范围找rows = 180000,说明候选集不小key = idx_status,说明先走的是状态索引Using filesort,说明后面还得额外排序
读到这里,慢点基本就坐实了:
- 前面扫得不够省
- 后面排得也不便宜
比逐列背字段更有用的,是这种读图顺序
以后再拿到一份 EXPLAIN,如果你不想被字段细节带跑,我更建议先按这个顺序读:
第 1 步:先看 type
先判断这条 SQL 访问路径粗不粗。
第 2 步:再看 rows
判断为了拿这点结果,它准备扫多少数据。
第 3 步:再看 key
确认真正走的索引路径,是不是一条合理的路。
第 4 步:最后看 Extra
确认后面有没有排序、临时表或别的额外成本。
如果这四步已经把问题说清楚了,通常就没必要再把每一列都上成一节课。
因为真实排查里,你真正要的不是一份 EXPLAIN 字段百科,而是一个足够快的判断:
- 这条 SQL 慢在扫描
- 还是慢在索引选路
- 还是慢在排序和中间处理
最后总结
EXPLAIN 最怕被读成两种极端:要么只剩一句“走了索引”,要么变成一张字段释义表。
更实用的方式其实简单得多:
先用
type看访问方式够不够粗,再用rows判断扫描重不重,然后看key确认索引路径,最后用Extra把排序和额外处理补齐。
只要按这条线去读,很多执行计划不用看太久,你就能很快判断:这条 SQL 真正慢在什么地方。