Explain 执行计划详解:一条慢 SQL 到底慢在哪,索引为什么会失效

真正会用 Explain 的人,不是能把字段含义背下来的人,而是能顺着执行计划,把一条 SQL 的访问路径讲明白的人。

很多人第一次学 MySQL Explain,都会很快进入一种“背字段模式”。

看到执行计划,先找 key 是不是 NULL,再看有没有走索引;看到 type=ALL 就紧张,看到 Using filesort 就觉得完了,最后把整份 Explain 学成一张“字段翻译表”。

但真实项目里,SQL 优化往往不是这样做的。

真正让人头疼的场景通常是:

  1. 索引明明建了,查询还是慢。
  2. Explain 里明明显示用了索引,但接口时间还是很难看。
  3. 两条看起来很像的 SQL,执行代价却完全不一样。
  4. 一改成 select *like '%xx%'date(create_time),性能就明显掉下来了。

我后来慢慢意识到,Explain 的核心价值,从来不是告诉你“有没有索引”,而是告诉你:

  1. 这条 SQL 打算怎么走。
  2. 它为什么要这么走。
  3. 它慢在扫描、过滤、排序,还是回表。
  4. 你下一步到底应该改 SQL,还是改索引。

这篇文章我会把内容重新按“实战排查”的视角来讲,不再只是字段罗列,而是按下面这个顺序:

  1. 先用几张图把 Explain 背后的索引逻辑补齐。
  2. 再讲 Explain 真正该重点看的字段。
  3. 再用一组高频 SQL 场景,把索引失效和执行计划读法串起来。
  4. 最后补上 MySQL 8+ 更值得掌握的新能力。

如果你最近正在补 MySQL 性能优化,这篇文章尽量让你看完之后,不只是“知道”,而是“能上手分析”。

1. 为什么 Explain 值得重学

很多人对 Explain 的第一印象是:“查执行计划的工具。”

这个定义没错,但它太轻了。

如果把 SQL 查询看成一次出行,Explain 更像一张路线图。它不负责告诉你结果是什么,而是告诉你数据库准备怎么到达结果。

同样是“从 A 点到 B 点”:

  1. 有的人走高速,代价低。
  2. 有的人绕远路,扫描了很多无效数据。
  3. 有的人虽然上了高速,但最后下匝道又堵住了,比如回表、排序、临时表。

所以 Explain 真正要看的,从来不是“是否用了索引”这一个点,而是整条访问路径是否合理。

2. 先别急着看字段,先把索引背后的 3 张图补齐

如果不先理解索引结构,后面很多 Explain 现象都会变成死记硬背。

2.1 B+Tree 为什么适合范围查找

在这里插入图片描述

这张图特别适合用来理解两个问题:

  1. 为什么 MySQL 索引特别擅长等值查询和范围查询。
  2. 为什么一旦你破坏了索引本身的有序性,优化器就很难继续走这条路径。

核心点在于:

  1. 非叶子节点主要存键值和指针,不存完整数据,所以一页能放更多索引项。
  2. 叶子节点天然有序,并且通常彼此相连,所以范围扫描会很高效。
  3. 只要条件还能顺着这棵树往下定位,索引就能发挥价值。

也正因为这样,像 where id > 100betweenlike 'abc%' 这种条件通常比较适合索引;而 like '%abc'date(create_time) 这种写法,则很容易让优化器放弃这条有序路径。

2.2 联合索引为什么强调最左前缀

在这里插入图片描述

很多人把“最左前缀法则”当口诀来背,但真正有用的是理解它为什么存在。

假设有联合索引:

KEY idx_name_age_position (name, age, position)

那么这棵索引树的组织顺序,其实就是:

  1. 先按 name 排。
  2. name 相同,再按 age 排。
  3. nameage 都相同,再按 position 排。

所以你写:

WHERE name = 'LiLei' AND age = 30

优化器能顺着树很快定位。

但如果你写:

WHERE age = 30 AND position = 'dev'

你等于一上来就跳过了入口列 name,数据库很难高效利用这棵树的有序性。

这就是为什么“最左前缀”不是玄学,而是 B+Tree 排序规则的直接结果。

2.3 为什么 select * 往往更贵:覆盖索引和回表

在这里插入图片描述

InnoDB 下最容易被忽略的一件事是:二级索引叶子节点里存的不是整行数据,而是“索引列 + 主键值”。

这会直接带来两种完全不同的查询路径:

  1. 如果你查询的字段刚好都在二级索引里,那就是覆盖索引,数据库可以直接返回结果。
  2. 如果你还要别的列,数据库就得先命中二级索引,再根据主键回到聚簇索引里把整行捞出来,这就是回表。

为什么这件事重要?

因为很多 SQL 慢,不是“没走索引”,而是“虽然走了索引,但回表太多”。

这一点后面我们会专门再展开。

3. Explain 到底是什么:它不是结果,而是访问路径的预判

Explain 本质上是让优化器把“准备怎么执行这条 SQL”先展示给你看。

所以它最适合解决的问题是:

  1. 先判断执行路径对不对。
  2. 再决定优化动作该落在 SQL 还是索引上。

我现在读 Explain,一般不会从左到右把所有列都看一遍,而是先按下面这条路线走。

在这里插入图片描述

这张图背后对应的是一个很实用的顺序:

  1. 先看 type,判断访问级别是不是太差。
  2. 再看 keykey_len,确认索引到底有没有真正用透。
  3. 再看 rowsfiltered,判断扫描成本是否过高。
  4. 最后看 Extra,判断是不是慢在排序、分组、回表、临时表这些额外动作上。

如果你一上来先盯着 key 看,很多时候会误判。因为“用了索引”和“用了一个合适的索引”,完全不是一回事。

4. 先放一张示例表,后面所有场景都围绕它来讲

为了让后面的例子更顺,我们先用一张很典型的员工表:

CREATE TABLE employees (
  id INT NOT NULL AUTO_INCREMENT,
  name VARCHAR(24) NOT NULL DEFAULT '' COMMENT '姓名',
  age INT NOT NULL DEFAULT 0 COMMENT '年龄',
  position VARCHAR(20) NOT NULL DEFAULT '' COMMENT '职位',
  hire_time DATETIME NOT NULL COMMENT '入职时间',
  PRIMARY KEY (id),
  KEY idx_name_age_position (name, age, position),
  KEY idx_hire_time (hire_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

我们重点关注两个索引:

  1. 主键 PRIMARY KEY (id)
  2. 联合索引 idx_name_age_position (name, age, position)

为什么只用这张表就够了?

因为单表已经足够把 Explain 最常见的 80% 场景吃透:

  1. 主键查询
  2. 联合索引最左前缀
  3. 范围条件截断
  4. 函数导致索引失效
  5. 覆盖索引和回表

后面我会再补一个 orders 场景,专门讲 Using filesortUsing temporary

5. 真正值得重点看的字段,不是全部,而是这几组

Explain 字段不少,但实战里真正值得花力气看的,主要是下面这些。

字段 它在回答什么问题 你应该怎么读 常见误区
id 谁先执行,谁后执行 id 越大通常优先级越高;相同 id 一般从上到下执行 只会背顺序,不会放到子查询和 union 里理解
select_type 这是简单查询还是复杂查询 重点识别 SIMPLEPRIMARYSUBQUERYDERIVED 看到子查询就慌,其实关键还是看访问路径
type 数据是怎么找到的 核心看访问级别,经验上至少尽量达到 range,更理想是 ref / eq_ref ALL 机械等同于“绝对有问题”
possible_keys 理论上哪些索引可能可用 只是候选,不等于真的会用 以为这里有值,key 就一定不为空
key 实际用了哪个索引 先确认有没有用索引,再确认是不是你希望的那条 只看 key,不看 key_len
key_len 联合索引到底用了多少 能帮你判断索引是否只用到了前几列 以为“用了联合索引”就等于“用满了联合索引”
rows 预计要扫描多少行 这是成本判断的重要依据 只看返回行数,不看扫描行数
filtered 扫描到的数据有多少比例能通过过滤 能帮助你判断过滤动作是不是发生得太晚 把它当成结果集准确统计值
Extra 有没有额外动作 重点看 Using indexUsing whereUsing filesortUsing temporary 看到 Using where 就误以为是坏事

这一张表如果要压成一句话,我会这样记:

  1. type 看访问级别。
  2. key / key_len 看索引是否用对、用透。
  3. rows / filtered 看成本。
  4. Extra 看额外代价。

6. 从 6 个高频场景里,看懂 Explain 到底该怎么用

下面这些场景,几乎是平时排查 SQL 时最常见的一批问题。

需要说明一下,rowsfilteredExtra 的具体细节会受到版本、数据量和统计信息影响,所以这里重点讲“现象”和“分析思路”,不把某一个数值写死。

6.1 场景一:主键等值查询,为什么通常最稳

EXPLAIN
SELECT *
FROM employees
WHERE id = 1001;

这类 SQL 一般是单表查询里最理想的类型之一。

你通常会看到的现象是:

  1. key=PRIMARY
  2. rows 很小,很多时候接近 1
  3. type 非常好,通常接近 const

为什么它这么稳?

因为主键本身就是最强定位条件。优化器不用扫一大片范围,只需要沿着聚簇索引快速命中目标行。

这类 SQL 给我们的启发不是“主键真好”,而是:当你的查询条件能把扫描范围压得足够小,执行计划通常就不会太差。

6.2 场景二:联合索引全值匹配,为什么这是最舒服的单表场景

EXPLAIN
SELECT name, age, position
FROM employees
WHERE name = 'LiLei'
  AND age = 22
  AND position = 'manager';

这条 SQL 的舒服之处在于,它和联合索引的顺序完全一致:

  1. 先按 name
  2. 再按 age
  3. 再按 position

这时你应该重点看两个点:

  1. key 是否命中了 idx_name_age_position
  2. key_len 是否足够长,说明三个列都被真正利用到了

如果查询列也刚好都在索引里,那 Extra 还有机会出现 Using index,这意味着它不需要回表,代价会更小。

很多人喜欢用一句话概括这个场景:命中联合索引。

但我更愿意把它理解成:优化器既找到了正确的入口,也顺着正确的顺序把范围压到了足够小。

6.3 场景三:跳过最左列,为什么索引像“突然没用了一样”

EXPLAIN
SELECT *
FROM employees
WHERE age = 22
  AND position = 'manager';

这类 SQL 很容易让人困惑:“我不是明明建了 (name, age, position) 吗,为什么效果还是一般?”

问题就出在你跳过了最左列 name

对于这棵索引树来说,name 是第一层组织顺序。你不先给出 name,数据库就很难直接定位到一小段连续范围内去找数据。

这时常见的结果是:

  1. 要么走不了你期待的联合索引。
  2. 要么即使看起来用了索引,利用程度也很有限。
  3. rows 可能明显上升。

所以“最左前缀”真正要解决的问题,不是记忆规则,而是理解索引树的入口。

如果业务里这类查询非常高频,就别指望原来的联合索引兼顾一切了,往往需要为新场景补一条更合适的索引。

6.4 场景四:在索引列上做函数,为什么执行计划会变难看

先看这条:

EXPLAIN
SELECT *
FROM employees
WHERE DATE(hire_time) = '2018-09-30';

很多项目里的慢 SQL,都是这样写出来的。

从业务角度看,这条 SQL 当然没问题;但从索引角度看,它的问题很明显:你不是直接拿 hire_time 去匹配,而是先对它做了一次 DATE() 计算。

一旦你把索引列加工了,数据库就很难继续沿着原本的有序索引去定位。

更稳的写法通常是改成范围查询:

EXPLAIN
SELECT *
FROM employees
WHERE hire_time >= '2018-09-30 00:00:00'
  AND hire_time < '2018-10-01 00:00:00';

这两条 SQL 的语义很接近,但执行计划可能完全不同。

根本原因不是第二条“更高级”,而是它保留了索引列原本的有序性,让优化器有机会做范围扫描。

这类问题在真实项目里特别常见:

  1. DATE(create_time)
  2. YEAR(create_time)
  3. LEFT(name, 3)
  4. 隐式类型转换

你可以把它们统一理解成一类问题:不是索引没建,而是你把索引入口给改坏了。

6.5 场景五:为什么 select * 往往会把一条本来还不错的 SQL 拖慢

先看这条:

EXPLAIN
SELECT name, age, position
FROM employees
WHERE name = 'LiLei'
  AND age = 23;

再看这条:

EXPLAIN
SELECT *
FROM employees
WHERE name = 'LiLei'
  AND age = 23;

这两条 SQL 看起来只是“查的列多一点、少一点”,但从执行路径上看,它们可能完全不是一个成本级别。

下面这张图可以直观看这个差别:

在这里插入图片描述

第一条查询更容易形成覆盖索引,因为 nameageposition 都已经在 idx_name_age_position 里。

这意味着:

  1. 数据库命中二级索引后,直接就能把结果返回出来。
  2. Extra 更容易出现 Using index

而第二条 select * 则很可能需要回表:

  1. 先命中二级索引。
  2. 再根据主键去聚簇索引里把整行数据拿回来。

当数据量很小时,这个差别不明显;但一旦扫描行数上来,回表带来的随机 IO 成本就会非常明显。

所以我现在对 select * 的态度一直比较谨慎:

  1. 不是绝对不能写。
  2. 但它不应该成为默认写法。
  3. 如果一条 SQL 本来有机会走覆盖索引,select * 往往就是把这份红利主动放掉了。

6.6 场景六:为什么用了索引,还是会出现 Using filesortUsing temporary

很多人学到这一步会发现另一个困惑:

“我的 SQL 不是已经走索引了吗,为什么 Extra 里还是有 Using filesortUsing temporary?”

这通常发生在排序和分组场景里。

比如下面这类查询:

EXPLAIN
SELECT *
FROM orders
WHERE order_status = 2
ORDER BY total_amount DESC
LIMIT 20;

哪怕你在 order_status 上有索引,这条 SQL 也未必能靠索引直接把排序做完。

原因很简单:

  1. 过滤条件用的是 order_status
  2. 排序条件用的是 total_amount

如果没有一条能够同时兼顾“过滤顺序”和“排序顺序”的索引,数据库就很可能先筛出一批数据,再额外排序,于是你会看到 Using filesort

再比如分组:

EXPLAIN
SELECT city, COUNT(*) AS order_cnt
FROM orders
WHERE pay_status = 1
GROUP BY city
ORDER BY order_cnt DESC;

这类 SQL 很容易在 Extra 里出现 Using temporary,因为数据库需要先聚合,再生成中间结果,最后还要排序。

这时真正应该想的不是“为什么它这么笨”,而是:

  1. 当前索引有没有同时覆盖过滤列和分组列。
  2. 聚合后的排序能不能被索引天然接住。
  3. 这是不是本来就更适合报表表、汇总表、缓存层来承接的查询。

也就是说,Using filesortUsing temporary 不一定代表你写错了 SQL,但它们一定是在提醒你:这里有额外成本。

7. 为什么“有索引”不等于“会走索引”,更不等于“性能就一定好”

这是很多人学 SQL 优化时最容易掉进去的误区。

数据库选择执行路径,看的是成本,不是面子。

下面这些情况,都可能导致“明明建了索引,但优化器不愿意按你想的方式走”:

7.1 选择性太差

如果一个条件一查就命中表里很大一部分数据,优化器可能会判断:既然反正要拿很多行,不如直接扫表。

7.2 范围太大

比如:

WHERE age >= 1 AND age <= 2000

如果这个范围本身就很大,优化器会重新计算成本,索引未必占优。

7.3 对索引列做了函数、运算或隐式转换

这会破坏索引列原本的有序性,前面已经讲过,这是最典型的一类索引失效原因。

7.4 ORIN!=NOT INNOT EXISTS

这些条件不是绝对不能用索引,但它们经常会让执行路径变复杂,优化器也更容易在多个候选路径之间权衡后选择别的方案。

7.5 回表代价太高

有时 key 明明不是 NULL,但整条 SQL 还是很慢,原因可能不是没索引,而是回表太多。

7.6 统计信息不准确

优化器做决策依赖统计信息。如果统计信息不准确,也可能导致路径判断不理想。

所以 SQL 优化最怕的一种思维就是:

  1. 建索引
  2. Explain
  3. 看到 key 不为空
  4. 觉得已经优化完成

这套流程太粗了。真正的关键,是看这条索引路径到底有没有把扫描范围、过滤成本和额外动作一起压下来。

8. 我现在更推荐的 Explain 排查顺序

以前我读执行计划,习惯从左到右一列列看。后来发现这样很容易陷进细节里,却抓不住主线。

现在我更愿意按下面这套顺序排查:

第一步:先看 type

先判断访问方式是不是太差。

如果一上来就是 ALL,先别急着下结论,但至少说明你该警觉了。

第二步:再看 keykey_len

确认到底用了哪条索引,以及联合索引是不是只用了前一部分。

很多“看起来用了索引”的 SQL,其实只是“浅浅用了第一列”。

第三步:再看 rows

这是判断成本的关键线索。

结果只返回 1 行,不等于它只扫描了 1 行。真正贵的地方,往往就藏在这里。

第四步:最后看 Extra

重点关注:

  1. Using index
  2. Using where
  3. Using index condition
  4. Using filesort
  5. Using temporary

这里最重要的不是死记硬背它们的字面意思,而是把它们翻译成“数据库额外做了什么事”。

9. MySQL 8+ 之后,更值得补进来的 Explain 能力

如果你现在学 Explain,还停留在很早期的旧资料里,那我很建议把下面这几件事一起补上。

9.1 普通 EXPLAIN 依然是入口,但不再是终点

普通 EXPLAIN 依然很重要,因为它给你的是执行前的路线预判。

但如果你只停留在这里,很多时候就只能“猜”问题在哪。

9.2 FORMAT=TREE 更适合看执行路径

相比传统表格结果,FORMAT=TREE 更适合用来观察优化器的执行层级。

例如:

EXPLAIN FORMAT=TREE
SELECT *
FROM employees
WHERE name = 'LiLei';

你会更容易从树形结构里看出:

  1. 先做了什么
  2. 后做了什么
  3. 哪一步是过滤,哪一步是扫描

对于刚从“字段阅读”转向“路径阅读”的同学,这个格式很有帮助。

9.3 FORMAT=JSON 更适合做结构化分析

如果你想把执行计划接到平台、脚本或工具里,FORMAT=JSON 会更友好,因为它更适合结构化解析。

例如:

EXPLAIN FORMAT=JSON
SELECT *
FROM employees
WHERE name = 'LiLei';

这对做 SQL 巡检、自动化分析或者内部诊断平台都很有价值。

9.4 EXPLAIN ANALYZE 最大的价值,不是“更高级”,而是“能验证”

这是我最建议补进来的能力。

EXPLAIN ANALYZE
SELECT *
FROM employees
WHERE name = 'LiLei';

它和普通 EXPLAIN 的差别在于:

  1. 普通 EXPLAIN 看的是优化器“预计怎么做”
  2. EXPLAIN ANALYZE 看的是数据库“实际怎么做”

这一步为什么重要?

因为很多 SQL 的问题,恰恰就出在“预估和实际不一致”。

不过这里要特别提醒一句:

EXPLAIN ANALYZE 会实际执行查询,所以在线上对重 SQL 使用时一定要谨慎。

9.5 旧资料里常见的 EXPLAIN EXTENDED,现在不用当重点了

如果你在一些老资料里还能看到 EXPLAIN EXTENDED,知道它是老语法背景就够了,不需要把它当成今天学习的重点。

对现在的日常分析来说,更值得掌握的是:

  1. 普通 EXPLAIN
  2. FORMAT=TREE
  3. FORMAT=JSON
  4. EXPLAIN ANALYZE

10. 3 个最容易踩的 Explain 误区

10.1 误区一:key 不为空,就说明 SQL 已经优化好了

不对。

key 不为空,只能说明数据库选了一条索引路径,不代表这条路径就是最优的,更不代表扫描行数、回表次数和排序代价都已经合理。

10.2 误区二:看到 ALL 就一定是严重问题

也不对。

小表全表扫描,有时候比走索引还划算。判断问题要结合表大小、rows、查询频率和整体代价。

10.3 误区三:索引优化就是“多建几个索引”

这更危险。

索引不是越多越好。你真正需要的是:

  1. 让高频查询有合适入口
  2. 让联合索引顺序尽量贴合查询条件
  3. 让排序、分组、覆盖索引也被一起考虑进去

11. 如果把整篇文章压成一份排查清单,我会这么用

线上遇到慢 SQL 时,我现在更习惯按这份清单过一遍:

  1. 先跑 EXPLAIN,不要先猜。
  2. 先看 type,判断访问方式是不是太差。
  3. 再看 keykey_len,判断索引有没有用对、用透。
  4. 再看 rows,判断扫描成本是不是过高。
  5. 再看 Extra,判断是不是慢在排序、分组、临时表或回表。
  6. 对照 SQL 本身,排查是不是有函数、隐式转换、select *、前置通配符、过大范围、OR 等问题。
  7. 再决定是改 SQL 写法,还是补索引,还是重构查询路径。
  8. 如果是 MySQL 8+,再用 EXPLAIN ANALYZE 验证预估和实际是否一致。

这套顺序最大的价值,是让 SQL 优化不再只是“凭经验试错”,而更像一个有步骤的工程过程。

12. 写在最后

我现在越来越觉得,Explain 真正难的地方,从来不是记住每一列的名字,而是把执行计划翻译成一条完整的访问路径。

你要能看出:

  1. 数据是怎么找到的。
  2. 索引为什么有效,或者为什么失效。
  3. SQL 是慢在扫描、过滤、回表,还是排序和分组。
  4. 下一步应该改 SQL、改索引,还是换查询思路。

如果把这篇文章再压缩成一句话,那就是:

Explain 的重点,不是“有没有索引”,而是“这条 SQL 到底是怎么跑的”。

当你真正开始顺着这条线去看执行计划时,索引优化这件事就不会再只是背口诀,而会慢慢变成一件能分析、能解释、也能落地的事。

参考资料

  1. MySQL 8.0 Reference Manual - EXPLAIN Statement
  2. MySQL 9.6 Reference Manual - Obtaining Information with EXPLAIN ANALYZE
  3. MySQL 9.6 Reference Manual - Obtaining Execution Plan Information
  4. MySQL Blog - MySQL EXPLAIN ANALYZE in JSON format
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐