Java

MySQL EXPLAIN 详解:别背字段,先看出这条 SQL 慢在哪

EXPLAIN 真正有用的地方,不是把 type、rows、key、Extra 一列列背下来,而是借这几个信号尽快判断:这条 SQL 到底慢在扫描过重、索引路径不对,还是排序和临时处理把成本抬高了。

  • MySQL
  • EXPLAIN
  • SQL优化
  • 数据库
10 分钟阅读

做 SQL 优化时,EXPLAIN 几乎总会最先被贴出来。

但很多人真正卡住的,不是不会跑 EXPLAIN,而是结果出来以后,还是不知道这条 SQL 到底慢在哪。

于是执行计划经常会被读成两种样子:

  • 要么只看见 key 不为空,就觉得“用了索引,应该问题不大”
  • 要么把 typerowskeyExtra 一列列解释得很完整,最后还是不知道该先改什么

我现在更习惯把 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

如果你按字段讲义的方式读,当然也能说:

  • typerange
  • key 用了 idx_status
  • rows 预估扫描 18 万行
  • Extra 里有 Using filesort

但真实排查时,更重要的是把它尽快翻译成一句人话:

  • MySQL 先沿着 status 把已支付订单捞出来
  • 这个候选集还是很大
  • 排序又没法顺着索引直接完成
  • 所以后面还要对一大批候选结果额外排序

到这一步,这条 SQL 的慢点其实已经很具体了:

不是“有没有索引”这么简单,而是先按一个区分度不够高的条件扫出很多数据,再做额外排序,结果只要 20 条,前面却已经干了不少活。

这就是我现在最在意的 EXPLAIN 读法:不是把每列都讲清楚,而是尽快坐实数据库到底准备怎么干活。

我通常只先看这四个信号

真在排一条慢 SQL 时,我一般不会从左到右把整张执行计划读完,而是先盯这四个点:

  • type
  • rows
  • key
  • Extra

因为大多数时候,慢点是不是已经很明显,就看这四个信号够不够把现场收住。

先看 type:这是精准找,还是已经扫开了

type 最重要的作用,不是让你记排名,而是先建立一个直觉:

  • 数据库这次是在精确定位
  • 还是已经退到比较粗的访问方式

consteq_refref,通常更像“找得比较准”;range 则要结合场景看;如果已经看到 indexALL,就该马上提高警惕。

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 的判断线其实很顺:

  1. type = range,说明不是精准命中,而是范围找
  2. rows = 180000,说明候选集不小
  3. key = idx_status,说明先走的是状态索引
  4. Using filesort,说明后面还得额外排序

读到这里,慢点基本就坐实了:

  • 前面扫得不够省
  • 后面排得也不便宜

比逐列背字段更有用的,是这种读图顺序

以后再拿到一份 EXPLAIN,如果你不想被字段细节带跑,我更建议先按这个顺序读:

第 1 步:先看 type

先判断这条 SQL 访问路径粗不粗。

第 2 步:再看 rows

判断为了拿这点结果,它准备扫多少数据。

第 3 步:再看 key

确认真正走的索引路径,是不是一条合理的路。

第 4 步:最后看 Extra

确认后面有没有排序、临时表或别的额外成本。

如果这四步已经把问题说清楚了,通常就没必要再把每一列都上成一节课。

因为真实排查里,你真正要的不是一份 EXPLAIN 字段百科,而是一个足够快的判断:

  • 这条 SQL 慢在扫描
  • 还是慢在索引选路
  • 还是慢在排序和中间处理

最后总结

EXPLAIN 最怕被读成两种极端:要么只剩一句“走了索引”,要么变成一张字段释义表。

更实用的方式其实简单得多:

先用 type 看访问方式够不够粗,再用 rows 判断扫描重不重,然后看 key 确认索引路径,最后用 Extra 把排序和额外处理补齐。

只要按这条线去读,很多执行计划不用看太久,你就能很快判断:这条 SQL 真正慢在什么地方。