上一篇我们讲了 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
因为:
全表扫描
不代表:
一定需要优化
真正要结合:
数据量
过滤比例
回表成本
排序成本
实际执行时间
一起判断。
网硕互联帮助中心
![2019年下半年网络管理员[案例分析]考试下午真题(答案+ 解析)-网硕互联帮助中心](https://www.wsisp.com/helps/wp-content/uploads/2026/09/20260909153309-6aa17c359df88-220x150.png)




评论前必须登录!
注册