MySQL EXPLAIN 妙用:执行计划分析与优化技巧

在 MySQL 开发中,遇到 SQL 执行缓慢时,最常用的分析工具之一就是 EXPLAIN。

它可以帮助我们回答几个关键问题:

  • MySQL 使用了哪个索引?

  • SQL 是全表扫描还是索引扫描?

  • 预计扫描多少行?

  • 多表 JOIN 的执行顺序是什么?

  • 是否发生了额外排序或临时表?

  • 优化器的估算是否准确?

MySQL 8.x 还提供了 EXPLAIN ANALYZE,可以进一步查看实际执行情况。

官方文档:MySQL EXPLAIN 官方文档


一、EXPLAIN 基本用法

最简单的使用方式:

EXPLAIN
SELECT *
FROM users
WHERE id = 10001;

也可以分析复杂 SQL:

EXPLAIN
SELECT o.id, o.amount, u.name
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.status = 1
ORDER BY o.created_at DESC
LIMIT 20;

常见输出:

+----+-------------+-------+------+---------------+---------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key     | key_len | ref   | rows | Extra       |
+----+-------------+-------+------+---------------+---------+---------+-------+------+-------------+
|  1 | SIMPLE      | users | const| PRIMARY       | PRIMARY | 8       | const |    1 |             |
+----+-------------+-------+------+---------------+---------+---------+-------+------+-------------+

实际分析时,不需要一开始记住所有字段,重点关注以下列:

type
possible_keys
key
key_len
rows
filtered
Extra

二、EXPLAIN 重要字段

2.1 id

表示查询中 SELECT 的编号。

简单查询:

SELECT * FROM users;

通常:

id = 1

复杂 SQL 包含子查询、UNION 时,可能出现多个 id。

一般来说:

id 主要用于判断查询结构和执行层级。


2.2 select_type

表示 SELECT 的类型。

常见:

SIMPLE
PRIMARY
SUBQUERY
DERIVED
UNION

最常见的是:

SIMPLE

表示简单查询。

这个字段在日常优化中的优先级通常低于 type、key、rows。


2.3 table

表示当前正在访问哪张表。

例如:

table = orders

多表 JOIN 时,可以通过这个字段观察 MySQL 的访问顺序。

注意:

SQL 中表的书写顺序,不一定就是 MySQL 实际执行的顺序。


2.4 type :判断访问方式

type 是 EXPLAIN 中非常重要的字段。

常见值:

system
const
eq_ref
ref
range
index
ALL

简单理解:

const / eq_ref
      ↓
ref
      ↓
range
      ↓
index
      ↓
ALL

但不要机械地认为越上面越好,最终还要结合 rows 判断。

const

例如:

SELECT *
FROM users
WHERE id = 10001;

如果 id 是主键:

type = const

通常非常快。

ref

例如:

SELECT *
FROM orders
WHERE user_id = 10001;

如果存在:

INDEX(user_id)

可能得到:

type = ref

表示通过普通索引进行等值查询。

range

例如:

SELECT *
FROM orders
WHERE created_at >= '2026-09-01'
  AND created_at < '2026-10-01';

通常:

type = range

表示索引范围扫描。

ALL

type = ALL

通常表示全表扫描。

例如:

SELECT *
FROM orders
WHERE remark = 'test';

如果 remark 没有合适索引,可能:

type = ALL

如果表有几百万甚至几千万数据,就需要重点关注。

但是小表出现 ALL 并不一定是问题。


2.5 possible_keys 和 key

这是分析索引时最重要的一组字段。

possible_keys

表示:

优化器认为可能使用的索引。

而:

key

表示:

最终实际选择的索引。

例如:

possible_keys = idx_user_id, idx_status
key           = idx_user_id

说明存在两个可能的索引,但最终选择了:

idx_user_id

特别注意一种情况:

possible_keys = idx_user_id
key           = NULL

这表示:

虽然存在索引,但优化器最终没有使用。

这时候不要马上 FORCE INDEX,应该先分析为什么。

例如:

status = 1

如果表中 90% 的数据都是 status = 1,走索引可能反而不如全表扫描。


2.6 key_len

key_len 表示 MySQL 实际使用的索引长度。

它对于分析联合索引到底使用到了哪些字段非常有价值。

例如:

INDEX idx_user_status_created
(
    user_id,
    status,
    created_at
)

查询:

WHERE user_id = 10001
  AND status = 1

通过 key_len 可以辅助判断索引使用到了哪一部分。

需要注意:

key_len 是字节长度,不是“使用了几个字段”。

具体长度还和字段类型、字符集、NULL 属性等有关。

因此遇到联合索引问题时,通常需要结合:

key
key_len
ref
rows

一起分析。


2.7 rows:非常重要

rows 表示优化器预计需要检查的行数。

例如:

rows = 20

和:

rows = 2,000,000

显然是完全不同的情况。

假设:

SELECT *
FROM orders
WHERE user_id = 10001;

EXPLAIN:

type = ref
key  = idx_user_id
rows = 500000

虽然:

type = ref

看起来不错,但实际上可能需要检查大量数据。

所以判断 SQL 是否合理时:

不要只看 type,一定要看 rows。


2.8 filtered

filtered 表示经过当前条件过滤后,预计有多少比例的记录符合条件。

例如:

rows = 100000
filtered = 10.00

可以粗略理解为:

100000 × 10%
≈ 10000

记录继续参与后续处理。

因此:

rows 很大
filtered 很低

往往意味着:

MySQL 找到了大量数据,但真正需要的数据很少。

这种情况就值得检查索引是否能够进一步缩小扫描范围。


2.9 Extra:发现问题的重要入口

Extra 经常能够直接告诉我们 MySQL 做了哪些额外工作。

常见结果:

Using where
Using index
Using index condition
Using filesort
Using temporary

Using where

表示还需要进行 WHERE 条件过滤。

它本身并不是问题。


Using index

通常表示使用了覆盖索引。

例如:

SELECT user_id, created_at
FROM orders
WHERE user_id = 10001;

如果索引:

INDEX(user_id, created_at)

那么查询需要的字段已经包含在索引中,就可能出现:

Using index

可以减少回表。


Using filesort

表示需要额外进行排序。

例如:

SELECT *
FROM orders
WHERE user_id = 10001
ORDER BY created_at DESC;

如果只有:

INDEX(user_id)

可能出现:

Using filesort

如果这个查询非常频繁,可以考虑:

INDEX(user_id, created_at)

但是否应该建立索引,最终还是要结合真实 SQL 和数据量判断。


Using temporary

表示查询过程中使用了临时表或临时结构。

常见于:

GROUP BY
ORDER BY
DISTINCT
UNION

复杂统计 SQL 中比较常见。

同样:

出现 Using temporary 不代表 SQL 一定有问题。

关键还是看数据量和实际执行时间。


三、EXPLAIN ANALYZE:查看真实执行情况

普通 EXPLAIN 如:

EXPLAIN
SELECT ...

主要告诉我们:

优化器预计怎么执行。

而:

EXPLAIN ANALYZE
SELECT ...

会真正执行 SQL,并返回实际执行情况。

例如:

EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE user_id = 10001;

重点关注:

estimated rows
actual rows
actual time
loops

最有价值的是:

estimated rows
        ↓
actual rows

例如:

estimated rows = 1000
actual rows    = 85000

说明:

优化器对数据量的估算严重偏差。

这时候就值得进一步检查:

ANALYZE TABLE orders;

以及数据分布、索引设计和查询条件。

官方文档:MySQL EXPLAIN ANALYZE 官方说明


四、EXPLAIN ANALYZE 与 EXPLAIN 的区别

可以简单理解:

命令作用
EXPLAIN查看预计执行计划
EXPLAIN ANALYZE实际执行并查看真实执行情况

日常优化推荐:

EXPLAIN
   ↓
发现执行计划异常
   ↓
调整 SQL / 索引
   ↓
EXPLAIN
   ↓
EXPLAIN ANALYZE
   ↓
验证实际效果

注意:

EXPLAIN ANALYZE 会真正执行 SQL。

对于:

UPDATE
DELETE
INSERT

等修改数据的语句,生产环境不要直接随意执行。


五、几个非常实用的 EXPLAIN 技巧

技巧 1:不要只看有没有索引

错误思路:

有索引 = SQL 很快

正确思路:

有没有使用合适的索引?
        ↓
扫描多少行?
        ↓
是否需要额外排序?
        ↓
实际执行多长时间?

技巧 2:优先关注 rows 很大的查询

例如:

type = ref
rows = 5000000

依然可能是慢 SQL。

所以:

type 好看,不代表 SQL 一定好。


技巧 3:关注 key = NULL

如果:

possible_keys = idx_user_id
key = NULL

就值得分析:

为什么没有使用?
数据选择性是不是太差?
统计信息是否准确?
SQL 是否导致索引失效?

技巧 4:关注 Using filesort

例如:

rows = 5000000
Extra = Using filesort

这比:

rows = 20
Extra = Using filesort

严重得多。

所以 EXPLAIN 一定要组合分析。


技巧 5:修改索引后一定重新 EXPLAIN

例如原来:

key  = idx_user_id
rows = 500000

增加联合索引:

CREATE INDEX idx_user_status_created
ON orders(user_id, status, created_at);

然后重新:

EXPLAIN
SELECT ...

重点比较:

key
key_len
rows
Extra

不要仅凭感觉判断优化是否成功。


六、推荐的 EXPLAIN 分析顺序

实际工作中可以按照这个顺序:

1. table
      ↓
2. type
      ↓
3. possible_keys
      ↓
4. key
      ↓
5. key_len
      ↓
6. rows
      ↓
7. filtered
      ↓
8. Extra

如果是复杂 SQL:

EXPLAIN
    ↓
分析执行计划
    ↓
检查索引
    ↓
调整 SQL / 索引
    ↓
EXPLAIN
    ↓
EXPLAIN ANALYZE
    ↓
对比实际执行结果

七、最后记住这几个重点

如果只记住 EXPLAIN 的核心内容,可以记:

type
→ MySQL 怎么访问数据?

key
→ 用了哪个索引?

key_len
→ 联合索引实际用了多少?

rows
→ 预计要检查多少行?

filtered
→ 过滤后还剩多少?

Extra
→ 有没有额外排序、临时表等操作?
EXPLAIN

回答的是:

“MySQL 打算怎么执行?”

EXPLAIN ANALYZE

回答的是:

“MySQL 实际是怎么执行的?”

因此,真正实用的 MySQL SQL 优化流程并不是单纯:

发现慢    → 加索引

而应该是:

慢 SQL
  ↓
EXPLAIN
  ↓
分析 type / key / rows / Extra
  ↓
调整 SQL 或索引
  ↓
EXPLAIN
  ↓
EXPLAIN ANALYZE
  ↓
确认实际执行效果

掌握这套流程,基本就具备了日常 MySQL 慢 SQL 的第一层排查能力。