Interview Lab / MySQL 索引三部曲 · 3/3

MySQL 执行计划:10 分钟看懂 EXPLAIN

执行计划是 MySQL 准备怎样查数据的步骤;EXPLAIN 是查看这些步骤和预计工作量的命令。本文面向会写 SQL(Structured Query Language,结构化查询语言)、知道索引是什么的后端开发,使用 MySQL 8.0 / 8.4 的普通表格输出。读完你应能判断用了哪个索引、怎样访问、预计扫描多少,以及是否还有额外操作。

EXPLAIN 四步分析法

1 · key最终用了哪个索引
2 · type怎样访问数据
3 · rows预计检查多少行
4 · Extra额外做了什么

最应记住:按顺序读完四项再判断;只看 key 有值,不能断言查询很快。

普通 EXPLAIN索引访问扫描行数MySQL 8.0 / 8.4
一、EXPLAIN 到底告诉你什么?

假设 orders 是订单表,user_id 是下单用户编号。给查询加上 EXPLAIN:

EXPLAIN
SELECT *
FROM orders
WHERE user_id = 100;

EXPLAIN 不关心 SQL 返回了什么业务数据;它告诉你 MySQL 准备怎么查。下面是这条查询的一份示意输出,具体值取决于表、索引和数据。

id 是查询步骤编号,select_type 是查询部分的类型,table 是当前访问的表;possible_keys 列出可能使用的索引,key_len 是所用索引键的长度,ref 是拿什么值与索引比较,filtered 是预计过滤后保留的比例。先不用背这些辅助列。

idselect_typetabletypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEordersrefidx_user_ididx_user_id8const20100.00NULL

SIMPLE 表示这是一段简单查询;const 在 ref 列表示拿常量 100 与索引比较;NULL 表示该格没有相应信息。现在只抓主线:用了 idx_user_id,按等值访问,预计检查约 20 行,没显示额外操作。

二、key:最终有没有用索引?

key 写的是最终选用的索引;possible_keys 只是可能用到的候选。以用户编号索引 idx_user_id 为例:

possible_keyskey该怎么读
idx_user_ididx_user_id最终用了这个索引。
idx_user_idNULL它是候选,但最终没有选。
NULLNULL当前这一步没有发现可用索引,也没有选用索引。
先看 key:判断最终用了哪个索引,不看 possible_keys。不过 key 非空只说明用了索引,扫描范围还要看 rows。
三、type:是怎样访问数据的?

type 是访问方式。日常先认识这六种,通常可按 const → eq_ref → ref → range → index → ALL 快速扫一眼。这个顺序是读法提示,不是绝对的性能排名。

type第一反应常见例子
const至多找到一行,常见于主键或唯一索引与常量比较。WHERE id = 1
eq_ref表连接时,对前面每行用完整唯一键查至多一行。用订单的用户编号连接用户表主键
ref普通索引等值查找,可能匹配多行。WHERE user_id = 100
range扫描索引的一段范围。WHERE create_time >= '2026-09-01'
index扫描整份索引,仍可能读很多记录。key: idx_xxx
ALL全表扫描。逐行检查 orders

ref / range

type: ref · key: idx_user_id · rows: 20 表示按用户编号等值找,预计查约 20 行。type: range · key: idx_create_time · rows: 5000 表示沿创建时间索引扫一段,预计约 5000 行。

index / ALL

type: index 虽然用了索引,却可能把整份索引扫完;type: ALL · key: NULL 是全表扫描,应结合 rows 关注。小表全表扫描也可能合理。

四、rows:预计要检查多少行?

做一个只换用户编号的对照:假设同一张 orders 表约有 1 亿笔订单,已有 idx_user_id;用户 100 约有 20 笔,批量下单账号 200 约有 80 万笔。分别执行:

EXPLAIN SELECT * FROM orders WHERE user_id = 100; -- SQL A
EXPLAIN SELECT * FROM orders WHERE user_id = 200; -- SQL B:只改编号

下面沿两条查询读一遍示意结果:访问方式和选中的索引相同,预计检查的行数却相差很大。

两条查询使用同一个索引,预计扫描量相差四万倍SQL A 查询用户 100,使用 idx_user_id 索引按 ref 等值访问,预计检查 20 行;SQL B 只把用户编号改为 200,使用相同索引和访问方式,却预计检查 800000 行。两条线索都从输入走到扫描量判断。 只改 user_id:同样走索引,扫描量为何不同? SQL Auser_id = 100key: idx_user_idtype: refrows: 20预计只查几十行SQL Buser_id = 200key: idx_user_idtype: refrows: 800000预计扫描范围大 数值来自上面的假设;两个 rows 都是 EXPLAIN 估算,不是真实扫描行数。
最应记住:同样选中索引、同样是 ref,预计扫描量仍可能相差四万倍;常见误读是把“走索引”直接等同于“查得快”。

rows 是优化器(MySQL 比较候选查法并选择计划的组件)的估算,并非真实扫描行数。读普通 EXPLAIN 时,先用它判断扫描量是否与数据规模和预期返回量相称。

key 告诉你用了什么索引;rows 帮你判断这个索引是否把预计扫描范围缩小到合理程度。
五、Extra:还有什么额外操作?

Extra 是附加提示,一格里可以同时出现多项。日常先认识下面五个:

提示应该怎么理解
Using index通常表示覆盖索引:查询要的列可以从索引取得,无需再读取完整数据行。它不是“使用了索引”的意思;是否用索引看 key。
Using where还有条件需要过滤。type: ref · key: idx_user_id · Extra: Using where 完全可能先用索引找候选行,再判断其余条件。
Using filesortMySQL 需要额外排序,通常要检查 ORDER BY 能否利用索引顺序。filesort 是这种排序过程的名称,不代表一定写磁盘。
Using temporary使用临时表处理中间结果;在 GROUP BY(分组)、DISTINCT(去重)、ORDER BY(排序)等查询中值得关注,但不等于一定有问题。
Using index condition索引条件下推,英文为 Index Condition Pushdown(ICP):Index 指索引,Condition 指条件,Pushdown 指把判断提前。以 InnoDB(MySQL 中负责保存表和索引的一种组件)为例,二级索引是主键索引之外的索引,记录中保存索引列与主键值;扫描时可先用索引中的条件过滤,再决定是否按主键读取完整行。
两个文字陷阱:Using index 讲的是覆盖;Using where 也不表示没有使用索引。
六、key_len 和 filtered:什么时候顺手看?

key_len 是索引键使用长度,可辅助判断联合索引(把多列按顺序组成一个索引)实际用到了多少前缀。例如定义了 INDEX idx_a_b_c(a, b, c),查询条件是 WHERE a = 1 AND b = 2;若 key 显示 idx_a_b_c,就把 key_len 与条件和列顺序一起看。新手无需死记字节数,也不要仅凭长度断言“用了几列”。

filtered 是当前条件过滤后,预计能继续传递的比例。例如 rows: 10000、filtered: 10.00,可粗略理解为 10000 × 10% ≈ 1000 行留下。它也是估算值,不抢前面四项的优先级。

七、拿到 EXPLAIN,我到底按什么顺序看?

照这个顺序读一行输出

1 · key最终用了哪个索引?还是 NULL?
2 · type是 ref / range,还是 index / ALL?
3 · rows预计检查几十行,还是几十万行?
4 · Extra有覆盖、额外排序或临时表吗?
5 · 对照 SQL结合条件、排序与索引列顺序解释结果。

如果查询涉及多张表,EXPLAIN 通常有多行:先对每一行按这套顺序读,再看它们合起来的工作量。本文案例先用一张表练熟基本读法。

八、四份输出,练习直接下判断

以下均为示意输出;每个案例只抓当前一行的结论,不把它当作真实耗时报告。

案例 1:正常走索引

type: ref              possible_keys: idx_user_id
key: idx_user_id       rows: 10
Extra: NULL

key 确认选了用户编号索引;ref 表示等值访问;预计约 10 行,没看到明显额外操作。整体看起来正常。

案例 2:没有走索引

type: ALL              possible_keys: NULL
key: NULL              rows: 1200000
Extra: Using where

这是全表扫描,预计检查约 120 万行,再用条件过滤。接着检查 WHERE 涉及的列有没有索引、条件写法是否妨碍使用索引,以及这类查询是否值得建立索引。

案例 3:用了索引,扫描仍然很大

type: range            possible_keys: idx_create_time
key: idx_create_time   rows: 850000
Extra: Using index condition

创建时间索引确实被选中,还发生了索引层过滤;但预计仍要检查约 85 万条索引记录。这里的问题不是“有没有索引”,而是范围是否太宽。

案例 4:过滤走索引,排序仍是额外步骤

type: ref              key: idx_user_id
rows: 20000            Extra: Using filesort

查询可能靠 idx_user_id 找候选行,但还要额外排序。过滤是否用索引,与 ORDER BY 能否利用索引顺序,是两件事;再对照 SQL 和索引定义确认原因。

九、最终速查表
字段或提示第一反应
possible_keys哪些索引可能使用
key最终用了哪个索引
type怎样访问数据
key_len联合索引使用前缀的辅助信息
rows预计检查多少行
filtered过滤后预计留下多少比例
Using index覆盖索引
Using where还有条件要过滤
Using filesort需要额外排序
Using temporary使用临时表
Using index condition使用索引条件下推(ICP)
key → type → rows → Extra

key 看有没有走索引;type 看怎么走;rows 看扫多少;Extra 看额外做了什么。

面试时可以这样说:“我先看最终选中的索引,再看访问方式和预计扫描行数,最后检查额外排序、临时表与覆盖索引。即使选了索引,如果预计还要扫很多记录,也不能据此说查询一定快。”

十、应用场景:什么时候用这四步?

订单查询突然变慢

什么时候用:先对同一条 SQL 执行 EXPLAIN。为什么适合:能迅速看出索引选择、预计扫描量和额外排序。注意:rows 是估算,不能当作实际耗时。

验收新建的索引

什么时候用:想确认查询是否选中新增索引。为什么适合:key 与 rows 能分别说明“选了谁”和“预计扫多少”。注意:用相同 SQL 和参数比较,别只看索引名。

什么时候不该硬用:小表全扫、查询本来就要返回大量行时,不能仅凭 ALL 或较大的 rows 判定必须加索引;普通 EXPLAIN 也不能直接给出真实耗时。

业务化表达:“订单列表变慢时,我先看这条查询是否选中用户索引、预计读多少订单、排序是否另做;如果已经选中索引但还要读几十万行,就继续检查查询范围,而不是只看见索引名就结束排查。”

十一、需要真实行数和耗时,再看 EXPLAIN ANALYZE

普通 EXPLAIN 展示的是优化器估算。以后要进一步确认真实扫描行数和耗时,可以使用 EXPLAIN ANALYZE;它会实际执行查询,因此应先评估语句的资源开销。本文先把普通 EXPLAIN 的四步读法练熟。

资料依据