云计算百科
云计算领域专业知识百科平台

MySQL EXPLAIN 怎么看?一条慢 SQL 到底慢在哪?

上一篇我们讲了 MySQL 索引为什么可能失效。

例如:

SELECT *
FROM user
WHERE YEAR(create_time) = 2026;

或者:

SELECT *
FROM user
WHERE name LIKE '%张';

我们知道,这些写法通常不利于普通 B+Tree 索引进行高效定位。

但实际开发中,不能只靠经验判断:

这个 SQL 应该走索引吧?

因为 MySQL 优化器最终可能选择:

全表扫描

也可能选择:

索引扫描

甚至某些看起来能走索引的 SQL,优化器也可能认为全表扫描更划算。

所以真正分析 SQL 时,我们需要一个工具:

EXPLAIN

1. EXPLAIN 是什么?

EXPLAIN 可以查看 MySQL 为一条 SQL 选择的执行计划。

例如:

EXPLAIN
SELECT *
FROM user
WHERE id = 10001;

它会告诉我们:

准备怎么查?

可能使用哪些索引?

最终选择哪个索引?

预计扫描多少行?

是否需要额外排序?

MySQL 在真正执行查询之前,先把自己的查询方案展示给我们。

普通 EXPLAIN 展示的是优化器选择的计划和估算信息,并不等于这条 SQL 已经实际执行完毕。

2. 先准备一张示例表

假设有一张订单表:

CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
status INT NOT NULL,
create_time DATETIME NOT NULL,
amount DECIMAL(10, 2) NOT NULL
);

并且建立联合索引:

CREATE INDEX idx_user_status_time
ON orders(user_id, status, create_time);

我们经常查询:

SELECT *
FROM orders
WHERE user_id = 10001
AND status = 1
ORDER BY create_time DESC
LIMIT 20;

接下来就用这张表理解 EXPLAIN。

3. EXPLAIN 最常看的几个字段

执行:

EXPLAIN
SELECT *
FROM orders
WHERE user_id = 10001
AND status = 1
ORDER BY create_time DESC
LIMIT 20;

会得到一张执行计划表。

不同 MySQL 版本、SQL 和数据分布,具体结果会不同,但最值得关注的是:

字段主要含义
type 访问表的方式
possible_keys 可能使用的索引
key 实际选择的索引
key_len 使用的索引键长度
rows 预计需要检查的行数
filtered 预计有多少比例的行能通过条件
Extra 额外执行信息

type

怎么查?

key

用哪个索引?

rows

预计查多少?

Extra

有没有额外操作?

4. type:MySQL 准备怎么查?

type 是入门阶段非常重要的字段。

它表示 MySQL 对这张表采用的访问方式。

常见的有:

system
const
eq_ref
ref
range
index
ALL

5. const:通过唯一条件找到一条记录

例如:

EXPLAIN
SELECT *
FROM orders
WHERE id = 10001;

因为:

id

是主键。

MySQL 可以通过主键索引直接定位到一条记录。

所以通常会看到:

type = const

可以简单理解成:

通过主键或唯一索引的等值条件,最多确定一条记录。

例如:

WHERE id = 10001

或者某个非空唯一索引上的等值查询。

这种访问方式通常非常高效。

6. ref:通过普通索引查找匹配记录

假设有:

CREATE INDEX idx_status
ON orders(status);

执行:

EXPLAIN
SELECT *
FROM orders
WHERE status = 1;

因为:

status

不是唯一字段。

可能有很多订单都是:

status = 1

所以 MySQL 需要找到一组匹配记录。

这时候可能出现:

type = ref

可以理解成:

通过非唯一索引的等值条件,查找一组匹配记录。

例如:

status = 1

可能匹配:

100 条

1000 条

10 万条

所以:

ref

不代表一定只查一条。

7. range:范围扫描

例如:

EXPLAIN
SELECT *
FROM orders
WHERE create_time >= '2026-01-01'
AND create_time < '2027-01-01';

如果 create_time 有合适索引,MySQL 可能使用:

type = range

也就是:

范围扫描

可以理解成:

B+Tree

定位起始位置

顺序扫描

到结束位置停止

常见范围条件包括:

>
<
>=
<=
BETWEEN
IN (...)

不过这些操作符并不保证一定出现 range,最终仍然取决于优化器选择。

8. index:扫描整个索引

这个字段特别容易误解。

很多人看到:

type = index

会认为:

太好了,使用索引了,肯定很快。

其实不一定。

index 通常表示:

扫描整个索引

例如:

B+Tree

从头开始扫描

一直扫描到后面

它和:

range

不一样。

range 是:

定位到某个范围

只扫描这个范围

而 index 可能是:

整个索引都要扫描

所以:

使用索引,不代表就一定是高效的索引查找。

有时候扫描整个二级索引比扫描整张表更便宜,但它仍然可能需要检查大量索引记录。

9. ALL:全表扫描

如果看到:

type = ALL

通常表示:

Full Table Scan

也就是全表扫描。

例如:

EXPLAIN
SELECT *
FROM orders
WHERE amount > 100;

假设 amount 没有索引。

MySQL 可能只能:

第 1 行

第 2 行

第 3 行

逐行检查。

但这里一定要记住:

ALL 不一定代表 SQL 写错了。

如果表只有:

100 行

全表扫描可能非常快。

如果查询需要返回:

90% 的数据

全表扫描也可能比大量回表更划算。

所以不能简单地:

type = ALL

必须加索引

而应该继续看:

rows

查询条件

返回数据量

实际执行耗时

10. type 怎么看?

const

唯一条件定位一条

ref

普通索引等值查找

range

索引范围扫描

index

扫描整个索引

ALL

全表扫描

一般来说:

精准定位

通常比大量扫描更有优势

但是真正快不快,还要看:

扫描多少数据

是否回表

是否排序

是否访问大量磁盘页

11. possible_keys:可能使用哪些索引?

假设:

CREATE INDEX idx_user_id
ON orders(user_id);

CREATE INDEX idx_status
ON orders(status);

执行:

EXPLAIN
SELECT *
FROM orders
WHERE user_id = 10001
AND status = 1;

优化器可能认为:

idx_user_id

idx_status

都有机会用于查询。

于是:

possible_keys

可能显示:

idx_user_id,idx_status

它的意思是:

这些索引可能用于查找这张表中的记录。

但并不代表:

这些索引全部都会使用

真正选择哪个,要看:

key

12. key:实际选择了哪个索引

例如:

possible_keys = idx_user_id,idx_status

key = idx_user_id

表示优化器最终选择:

idx_user_id

作为访问索引。

如果:

key = NULL

通常表示没有选择索引作为当前表的访问路径。

但是:

key 不为 NULL,也不代表这条 SQL 一定很快。

例如:

type = index

key = idx_user_id

可能仍然是在扫描整个索引。

所以:

key

要和:

type
rows
Extra

一起看。

13. key_len:联合索引用到了多少

联合索引:

CREATE INDEX idx_user_status_time
ON orders(user_id, status, create_time);

假设查询:

EXPLAIN
SELECT *
FROM orders
WHERE user_id = 10001
AND status = 1;

我们想知道:

联合索引用到了哪些列?

这时候可以参考:

key_len

它表示 MySQL 为所选索引使用的键长度,单位是字节。

例如:

只利用 user_id

利用 user_id + status

对应的 key_len 可能不同。

但这里要注意:

不要只靠 key_len 的数字,机械地判断“用了几个字段”。

因为它还会受到:

字段类型

是否允许 NULL

字符集

索引列定义

等因素影响。

而且 key_len 也不能完整说明某个字段究竟用于范围定位,还是用于后续过滤。

所以它更适合:

辅助判断联合索引使用情况

而不是单独作为最终结论。

14. rows:预计需要检查多少行?

这是分析慢 SQL 时非常重要的字段。

例如:

rows = 10

可以理解成优化器预计:

大约检查 10 行

而:

rows = 1000000

则表示预计需要检查:

大约 100 万行

如果一个 SQL:

type = ALL

rows = 1000000

那就值得重点关注。

但要注意:

普通 EXPLAIN 中的 rows 是估算值,不是实际执行后精确统计的行数。
优化器会根据统计信息估算成本,因此可能存在误差。

15. filtered:预计过滤后剩多少

假设:

rows = 10000

filtered = 10.00

可以简单理解成:

预计检查 10000 行

其中大约:

10%

能够通过当前表的条件。

也就是:

10000 × 10%
=
1000 行

可能继续进入后续处理。

所以:

rows

关注的是:

预计检查多少行

而:

filtered

关注的是:

预计有多少比例通过条件

不过这两个值都是估算,不要把它们当成真实执行结果。

16. Extra:有没有额外操作

Extra 是另一个非常值得关注的字段。

常见内容包括:

Using where

Using index

Using index condition

Using filesort

Using temporary

17. Using where:还需要过滤

例如:

SELECT *
FROM orders
WHERE status = 1;

执行计划中可能出现:

Using where

表示:

MySQL 还需要根据 WHERE 条件过滤记录

但这里要注意:

Using where 不代表没有使用索引。

例如:

type = range

key = idx_create_time

Extra = Using where

仍然可能已经利用索引完成了范围定位。

只是后面还需要继续判断某些条件。

所以不要看到:

Using where

就认为:

索引失效

18. Using index:覆盖索引

假设:

CREATE INDEX idx_user_status
ON orders(user_id, status);

执行:

SELECT user_id, status
FROM orders
WHERE user_id = 10001;

查询需要:

user_id

status

都在索引中。

因此 MySQL 可能直接从索引中获取结果:

二级索引

找到需要的字段

直接返回

不需要再回表读取完整行。

这就是:

覆盖索引

执行计划中可能出现:

Using index

所以:

Using index 通常表示查询所需的列可以直接从索引中获取。

但要注意:

type = index

和:

Extra = Using index

不是一回事。

前者:

扫描整个索引

后者:

覆盖索引

19. Using index condition:索引条件下推

CREATE INDEX idx_a_b_c
ON test(a, b, c);

执行:

SELECT *
FROM test
WHERE a = 1
AND b > 10
AND c = 3;

这里:

a = 1

和:

b > 10

可以帮助定位索引范围。

而:

c = 3

虽然通常不能继续缩小范围,但仍可能在索引层进行过滤。

如果 MySQL 使用了:

Index Condition Pushdown

执行计划中可能出现:

Using index condition

可以理解成:

扫描索引记录

先判断索引中能判断的条件

符合条件

再读取完整行

这样可以减少不必要的回表。

所以:

Using index condition 不是索引失效,而是 MySQL 利用索引信息进行提前过滤。

20. Using filesort:需要额外排序

假设:

SELECT *
FROM orders
WHERE status = 1
ORDER BY amount DESC;

如果没有合适索引帮助排序,MySQL 可能需要额外执行排序操作。

这时候 Extra 可能出现:

Using filesort

很多人看到:

filesort

会以为:

一定在磁盘上排序

其实不是。

filesort 是 MySQL 对额外排序操作的名称,并不代表一定使用磁盘文件。

它可能在内存中完成,也可能在数据量较大时涉及磁盘。

所以:

Using filesort 表示需要额外排序,不等于一定发生磁盘排序。

21. Using temporary:使用临时表

某些:

GROUP BY

DISTINCT

ORDER BY

复杂查询

可能需要临时表辅助处理。

例如:

SELECT status, COUNT(*)
FROM orders
GROUP BY status;

在某些执行计划下,可能看到:

Using temporary

表示 MySQL 使用了临时表进行中间结果处理。

但同样需要注意:

Using temporary 不代表 SQL 一定很慢。

如果数据量很小,临时表可能没有明显问题。

真正需要关注的是:

临时表处理的数据量

是否发生磁盘临时表

是否可以通过索引优化

22. 一个慢 SQL 怎么分析

现在来看一个比较典型的例子。

假设订单表有:

100 万条数据

并且建立:

CREATE INDEX idx_create_time
ON orders(create_time);

现在执行:

SELECT *
FROM orders
WHERE YEAR(create_time) = 2026;

假设 EXPLAIN 显示:

type = ALL

possible_keys = NULL

key = NULL

rows = 1000000

Extra = Using where

这里是一个示意执行计划,不是实际数据库测试结果。

我们可以这样分析。


第一步:看 type

type = ALL

说明:

全表扫描


第二步:看 key

key = NULL

说明:

没有选择索引作为访问路径


第三步:看 rows

rows = 1000000

说明优化器预计:

需要检查大量记录


第四步:看 SQL

YEAR(create_time) = 2026

发现:

对索引列使用了函数

普通 create_time 索引通常无法直接用于这个表达式的范围定位。

于是可以考虑改写。

23. 改写以后再看 EXPLAIN

改成:

SELECT *
FROM orders
WHERE create_time >= '2026-01-01'
AND create_time < '2027-01-01';

假设新的执行计划:

type = range

key = idx_create_time

rows = 50000

Extra = Using index condition

同样,这里只是示意结果,实际输出取决于数据和 MySQL 版本。

现在可以理解成:

原来:

全表扫描

预计检查 100 万行

改成:

索引范围扫描

预计检查 5 万行

说明改写有机会明显减少扫描量。

但最后还需要验证:

实际执行时间有没有下降?

因为:

EXPLAIN 只是分析工具,真正的优化结果还需要通过实际执行来验证。

24. EXPLAIN ANALYZE 又是什么?

普通:

EXPLAIN
SELECT ...

主要展示:

优化器选择的执行计划

而 MySQL 8.0.18 及以后支持:

EXPLAIN ANALYZE
SELECT ...

它会:

实际执行查询

并提供实际执行信息,例如:

实际耗时

实际读取行数

循环次数

因此可以用来比较:

优化器估算

和:

真实执行情况

例如:

EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE user_id = 10001;

这对于分析:

为什么优化器估算不准?

到底哪一步真正耗时?

非常有帮助。

不过要注意:

EXPLAIN ANALYZE 会实际执行语句,不要把它当成完全无副作用的普通 EXPLAIN。

尤其在生产环境中,需要注意查询成本和执行影响。

25. EXPLAIN 还有哪些值得知道的形式?

除了普通表格形式:

EXPLAIN
SELECT ...

MySQL 还支持:

EXPLAIN FORMAT=JSON
SELECT ...

JSON 格式会提供更加详细的执行计划信息。

例如:

访问路径

成本估算

使用的索引

过滤条件

排序信息

对于入门阶段:

普通 EXPLAIN

已经足够。

等以后分析复杂 JOIN、子查询或优化器成本时,再深入:

EXPLAIN FORMAT=JSON

会更加合适。

26. 实际开发中怎么看 EXPLAIN?

我建议按照这个顺序:

SQL 很慢

先看 type

是全表扫描还是索引访问?

看 key

实际用了哪个索引?

看 rows

预计扫描多少行?

看 Extra

是否有额外排序、临时表、索引条件下推?

结合 SQL 和索引结构分析

不要只盯着:

key

因为:

用了索引

不代表:

查询一定快

也不要只盯着:

type = ALL

因为:

全表扫描

不代表:

一定需要优化

真正要结合:

数据量

过滤比例

回表成本

排序成本

实际执行时间

一起判断。

赞(0)
未经允许不得转载:网硕互联帮助中心 » MySQL EXPLAIN 怎么看?一条慢 SQL 到底慢在哪?
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!