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 的第一层排查能力。