Skip to content

EXPLAIN 执行计划详解(type/Extra/rows 关键字段)

为什么 EXPLAIN 是 DBA 的第一道防线

线上 MySQL 慢查询,80% 的场景不需要打开 profiling,不需要抓堆栈,也不需要看 performance_schema。一条 EXPLAIN 就够了

EXPLAIN 是 MySQL 提供的执行计划查看工具,它告诉你优化器打算怎么执行你的 SQL。你不需要猜,不需要"我觉得这里应该走索引"——EXPLAIN 直接把决策摆在你面前。

但问题是,很多人 EXPLAIN 看完了,只知道有没有走索引,却看不懂 typeExtra 的深层含义。这篇文章把 EXPLAIN 输出中最关键的三个字段彻底讲清楚。

EXPLAIN 输出长什么样

先看一个例子:

sql
EXPLAIN SELECT * FROM orders WHERE user_id = 123 ORDER BY create_time DESC LIMIT 10\G

输出:

id: 1
select_type: SIMPLE
table: orders
type: ref
possible_keys: idx_user_id
key: idx_user_id
key_len: 8
ref: const
rows: 156
Extra: Using index condition; Using filesort

逐行解读:

  • type=ref:等值匹配,通过索引找到所有匹配的行,还不错
  • key=idx_user_id:实际用到的索引是 idx_user_id
  • rows=156:预估扫描 156 行(实际可能更多,但量级没问题)
  • Extra="Using index condition; Using filesort":用到了索引下推(ICP)来减少回表,但 ORDER BY create_time 走了文件排序——这是优化点

三个关键字段详解

1. type:访问类型,决定 SQL 效率的下限

type 从好到差的排序是:

system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

const / system

  • 主键或唯一索引等值查询,最多返回一行
  • WHERE id = 1 对主键的精准匹配
  • 这是 MySQL 最快的访问方式,没有之一
  • system 是 const 的特例——表只有一行(系统表常见)

eq_ref

  • 多表 JOIN 时,被驱动表使用主键或唯一索引等值匹配
  • SELECT * FROM t1 JOIN t2 ON t1.id = t2.id,t2 的访问类型就是 eq_ref
  • 对 JOIN 来说是最高效的

ref

  • 普通索引的等值匹配(非唯一)
  • WHERE user_id = 123,user_id 有索引但不是唯一索引
  • 可能返回多行,但走索引,性能可接受

ref_or_null

  • ref 的变体,额外还要查 IS NULL 的行
  • WHERE user_id = 123 OR user_id IS NULL
  • 比 ref 多一次 NULL 值的扫描,性能稍差

index_merge

  • 优化器同时使用多个索引,合并结果集
  • 常见于 OR 条件或多条件复合查询
  • 底层走 Intersection(交集)、Union(并集)、Sort-Union 三种合并策略
  • 坑:索引合并不一定是好事,有时候建联合索引比合并更高效

range

  • 索引范围扫描:>, <, BETWEEN, IN, LIKE 'abc%'
  • 比 ref 差,但仍在可接受范围
  • 注意:IN 列表如果太长(比如 IN 里面 1000 个值),优化器可能退化为 ALL

index

  • 全索引扫描,比全表扫描好一点,但也是灾难
  • 常见于 SELECT COUNT(*) 或覆盖索引但无法做范围过滤的场景
  • 意味着你遍历了整个索引树,虽然比全表扫描小,但数据量大了依然扛不住

ALL

  • 全表扫描,看到这个就应该警铃大作
  • 除了极少数情况(小表、数据量 < 100 行),必须优化

实战判断:看到 type=ALLtype=index,直接标记为慢 SQL,加入优化队列。type=reftype=range 是可以接受的,但需要结合 rowsExtra 进一步判断。

2. Extra:额外信息,暴露了 SQL 的隐藏问题

Extra 字段是 EXPLAIN 中最容易被忽视但最有价值的信息。它告诉你 MySQL 在处理这条 SQL 时额外做了什么

Using index(覆盖索引)

  • 查询所需的字段全部在二级索引中,不需要回表
  • 这是最好的 Extra,性能最好
  • 出现条件:索引的叶子节点包含了查询的所有字段

Using index condition(索引下推 ICP)

  • 用到了 MySQL 5.6 引入的 ICP 优化
  • 把 WHERE 条件中可以利用索引的过滤条件下推到存储引擎层
  • 注意:ICP 仍然需要回表,只是减少了回表的行数
  • 面试高频考点:Using index 和 Using index condition 的区别

Using where

  • 回表后,在 server 层又做了过滤
  • 通常意味着索引筛选不够彻底,有大量数据回表后被过滤掉
  • 如果能将过滤条件包含在索引中,可以升级为 Using index

Using filesort(文件排序)

  • MySQL 需要额外排序,且无法通过索引排序
  • 排序数据量小 → 在内存(sort_buffer)中排序
  • 排序数据量大 → 使用磁盘临时文件,性能急剧下降
  • 优化方向:让 ORDER BY 字段走索引排序
  • 踩坑:sort_buffer_size 设太大也会有问题——每个会话分配一份,高并发下内存爆炸

Using temporary(临时表)

  • MySQL 使用临时表来存储中间结果
  • 常见于 GROUP BYDISTINCT、子查询
  • 如果临时表太大,会写入磁盘,性能极差
  • 优化方向:给 GROUP BY 和 DISTINCT 的字段加索引,或者改写成 JOIN

Using index for group-by

  • MySQL 利用索引的排序特性直接完成 GROUP BY,无需临时表
  • 这是 GROUP BY 的最优方案

Using where; Using index

  • 查询条件在索引中过滤,但查询字段也在索引中(覆盖索引)
  • 不需要回表,不需要额外读取数据行
  • 这是第二好的 Extra(仅次于纯 Using index)

Using index for skip scan

  • MySQL 8.0.13+ 引入的 Skip Scan Range Access Method
  • 适用于联合索引但跳过前导列的场景
  • 比如索引 (a, b),查询 WHERE b = 1,MySQL 遍历 a 的不同值,对每个 a 值做 b 的范围扫描
  • 性能比全索引扫描好,但比正常的 range 差

3. rows:预估扫描行数

rows 是优化器估算的扫描行数。注意是估算,不是精确值。

rows 的应用场景

  • 比较同一个 SQL 加不同索引后的扫描行数变化
  • 结合 type 判断:type=refrows=100000,说明索引选择性差,扫描行数太多
  • 如果 rows 远大于实际返回行数(如 rows=100000,实际返回 10 行),说明索引过滤性不够

rows 的局限性

  • 不精确,统计信息可能过时(ANALYZE TABLE 可以刷新)
  • 不反映实际回表 IO 次数
  • 在多表 JOIN 场景下,每层的 rows 会相乘,最终估算可能偏差很大

更准确的工具:MySQL 8.0.18+ 的 EXPLAIN ANALYZE 提供实际执行耗时和循环次数。

代码实战:用 EXPLAIN 分析一条慢 SQL

假设我们有一张订单表,数据量约 500 万行,有一条慢查询:

sql
-- 慢 SQL
SELECT order_id, user_id, amount, status, create_time
FROM orders
WHERE status = 1
ORDER BY create_time DESC
LIMIT 10;

先看 EXPLAIN:

sql
EXPLAIN SELECT order_id, user_id, amount, status, create_time
FROM orders
WHERE status = 1
ORDER BY create_time DESC
LIMIT 10\G

输出:

id: 1
select_type: SIMPLE
table: orders
type: ALL
possible_keys: idx_status
key: NULL
rows: 4832100
Extra: Using where; Using filesort

问题诊断

  • type=ALL:全表扫描,483 万行
  • Extra=Using filesort:还要文件排序
  • 等于把整张表扫一遍再排序,肯定慢

优化方案:建联合索引 (status, create_time),让 WHERE 和 ORDER BY 都走索引

sql
ALTER TABLE orders ADD INDEX idx_status_time (status, create_time);

EXPLAIN SELECT order_id, user_id, amount, status, create_time
FROM orders
WHERE status = 1
ORDER BY create_time DESC
LIMIT 10\G

优化后输出:

id: 1
select_type: SIMPLE
table: orders
type: ref
possible_keys: idx_status_time
key: idx_status_time
key_len: 2
ref: const
rows: 523000
Extra: Using index condition

变化

  • type=ALLtype=ref:不再全表扫描
  • rows=4832100rows=523000:扫描行数减少到 1/9
  • Extra 去掉了 Using filesort:ORDER BY 走索引排序,无需额外排序

rows=523000 仍然偏高,因为 status=1 的记录占了 50 万行,而我们要的是 ORDER BY 后取前 10 条。这里还可以进一步优化:延迟关联

sql
EXPLAIN SELECT o.order_id, o.user_id, o.amount, o.status, o.create_time
FROM orders o
INNER JOIN (
    SELECT id FROM orders
    WHERE status = 1
    ORDER BY create_time DESC
    LIMIT 10
) AS tmp ON o.id = tmp.id\G

子查询部分:

id: 1
select_type: PRIMARY
table: <derived2>
type: ALL
rows: 10
Extra: NULL

子查询(id=2):

id: 2
select_type: DERIVED
table: orders
type: ref
key: idx_status_time
rows: 10
Extra: Using index

核心原理:子查询用覆盖索引(Using index)只扫描 id 和排序字段,找到 top 10 的 id,再回表取出完整数据。回表次数从 523000 次降到 10 次,这才是真正的优化精髓。

Agent 场景类比:EXPLAIN 就像 LLM 的 Chain-of-Thought

如果你做过 AI Agent 的 prompt 调优,你一定遇到过 LLM 不按你的指令走,你不知道它中间在想什么。这时候你会用 Chain-of-Thought 让模型暴露推理过程。

EXPLAIN 就是数据库的 Chain-of-Thought

  • LLM 的 CoT 输出是"我要先理解问题,然后检索知识,最后生成回答"
  • 数据库的 EXPLAIN 输出是"我要先走 idx_status_time 索引过滤 status=1 的记录,再用索引排序,然后回表取 10 行数据"

两者都让你从黑盒变成白盒。你不需要猜测,不需要等待执行完才知道结果——执行前你就知道它打算怎么干了。

面试官问你"MySQL 怎么优化一条慢 SQL",你回答"先 EXPLAIN,然后看 type 和 Extra 判断问题,再建索引"——这跟 Agent 调优时先看 CoT 再改 prompt 是一个思维模式。

高级实战:三表 JOIN 的性能分析

这是我在线上真实遇到过的场景。一个 Agent 平台的后台,需要查用户最近 30 天的 Agent 调用记录,涉及三张表:agents(Agent 定义)、sessions(会话)、invocations(调用记录)。

sql
-- 慢 SQL,页面加载卡死
SELECT a.name, s.session_id, i.input_tokens, i.output_tokens, i.latency_ms, i.created_at
FROM agents a
JOIN sessions s ON a.agent_id = s.agent_id
JOIN invocations i ON s.session_id = i.session_id
WHERE a.owner_id = 10086
  AND i.created_at >= '2026-06-23'
  AND i.latency_ms > 5000
ORDER BY i.created_at DESC
LIMIT 50;

EXPLAIN 输出:

id | select_type | table | type | possible_keys | key | rows | Extra
1  | SIMPLE      | a     | ref  | idx_owner_id  | idx_owner_id | 5    | Using index
1  | SIMPLE      | s     | ref  | idx_agent_id  | idx_agent_id | 120  | NULL
1  | SIMPLE      | i     | ALL  | idx_session_id | NULL | 2500000 | Using where; Using filesort

问题分析

  • a 表走索引,5 行,没问题
  • s 表走索引,120 行,也还好
  • i 表(invocations)不走索引,全表扫描 250 万行,还要 Using filesort 排序
  • 问题是优化器认为 idx_session_idi.latency_ms > 5000i.created_at 排序没有帮助,所以选择了全表扫描

优化方案:给 invocations 建联合索引 (session_id, created_at, latency_ms, input_tokens, output_tokens),让 WHERE 和 ORDER BY 都走索引,且覆盖查询的所有字段。

sql
ALTER TABLE invocations ADD INDEX idx_session_created_latency
  (session_id, created_at, latency_ms, input_tokens, output_tokens);

优化后 EXPLAIN:

id | select_type | table | type | possible_keys | key | rows | Extra
1  | SIMPLE      | a     | ref  | idx_owner_id  | idx_owner_id | 5    | Using index
1  | SIMPLE      | s     | ref  | idx_agent_id  | idx_agent_id | 120  | NULL
1  | SIMPLE      | i     | ref  | idx_session_created_latency | idx_session_created_latency | 1800 | Using index condition; Using index

变化

  • i 表从 type=ALL(250 万行)变成 type=ref(1800 行)
  • 去掉了 Using filesort,排序走索引
  • Extra=Using index condition; Using index:覆盖索引,不需要回表
  • 查询从 3 秒降到 20 毫秒

踩坑实录:EXPLAIN 的 5 个常见误区

误区 1:rows 小就一定快

。rows 是扫描行数预估,不是 IO 次数。type=index 配合 rows=100 走的是全索引扫描,可能比 type=ref 配合 rows=1000 更慢,因为索引扫描需要遍历整个索引树。

误区 2:用了索引就是好

type=index 也是用了索引,但它是全索引扫描。如果你看到 type=indexrows 很大,本质上跟全表扫描没区别。

误区 3:Extra 为空就是好

。Extra 为空意味着没有额外信息,不代表没有性能问题。type=ALL 且 Extra 为空,说明全表扫描且没有额外操作——但全表扫描本身就是问题。

误区 4:EXPLAIN 说走索引,线上就一定走

。EXPLAIN 是优化器基于当前统计信息的预估。线上的参数值、数据分布、并发情况都可能不同。possible_keys 里有索引但 key 为 NULL,说明优化器认为走索引不如全表扫描——可能是统计信息过时了,也可能是你的 SQL 写法让优化器"误判"。

误区 5:EXPLAIN ANALYZE 可以替代 EXPLAIN

对部分场景成立,但不完全。EXPLAIN ANALYZE 会实际执行 SQL,输出每一步的实际耗时和行数。它的优点是精确,缺点是:

  • 会实际执行,写操作需要包裹在事务里回滚
  • 大数据量下可能执行很久
  • 不适合线上直接执行

EXPLAIN 与 EXPLAIN ANALYZE 对比(MySQL 8.0.18+)

特性EXPLAINEXPLAIN ANALYZE
是否执行 SQL不执行执行
输出内容预估执行计划实际执行计划 + 实际耗时
精确度预估,可能偏差精确
执行风险会执行,写操作需回滚
适用场景日常分析、上线前生产问题定位、性能瓶颈确认

EXPLAIN ANALYZE 的输出示例:

-> Limit: 50 row(s)  (actual time=0.234..0.312 rows=50 loops=1)
    -> Sort: i.created_at DESC, limit input to 50 row(s) per chunk  (actual time=0.233..0.311 rows=50 loops=1)
        -> Nested loop inner join  (actual time=0.021..0.283 rows=1800 loops=1)
            -> Index lookup on a using idx_owner_id  (actual time=0.011..0.015 rows=5 loops=1)
            -> Nested loop inner join  (actual time=0.008..0.050 rows=1800 loops=5)
                -> Index lookup on s using idx_agent_id  (actual time=0.003..0.008 rows=120 loops=5)
                -> Index lookup on i using idx_session_created_latency  (actual time=0.001..0.005 rows=1800 loops=600)

每一行都有 actual time(实际耗时)、rows(实际行数)、loops(循环次数)。你一眼就能看出哪一步最耗时,而不是像 EXPLAIN 那样只能靠猜。

总结

字段判断标准
typesystem/const/eq_ref/refALL/index看见 ALL 必须优化
ExtraUsing indexUsing filesort/Using temporary两个 using 是提速信号,两个 using 是减速警报
rows小(< 1000)大(> 10000)结合 type 看,type 好但 rows 大说明索引选择性差

EXPLAIN 的核心用法:对比优化前后的执行计划,看 type 有没有提升、Extra 有没有出现 Using indexrows 有没有下降。不要只看绝对值看变化趋势

最后一条铁律:EXPLAIN 只是预估,线上慢 SQL 的最终判断标准是实际执行时间。分析完 EXPLAIN 后,用小数据量验证,分批上线,用 performance_schema 和慢查询日志做最终确认。

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。