执行计划分析:如何用EXPLAIN命令找到慢查询的根因
说实话,我见过太多开发同学一遇到查询慢,第一反应就是「加索引」。加完发现没效果,又跑来问我。我通常会反问一句:你看过执行计划了吗?大部分人沉默了。
执行计划,说白了就是数据库告诉你「我打算怎么查这张表」。你连数据库的计划都不看,怎么知道它走没走索引?走了哪个索引?扫了多少行?
这一章,我就带你把这个工具彻底用熟。
EXPLAIN 怎么用?
用法极其简单。在你的 SELECT 语句前面加上 EXPLAIN 关键字就行:
EXPLAIN SELECT * FROM orders WHERE order_id = 10086;
MySQL 会返回一张表,里面每一行对应一个查询步骤。嗯,就这么简单。但真正值钱的是怎么读这张表。
EXPLAIN ANALYZE,它会实际执行查询并告诉你每一步花了多少时间。这在排查真实慢查询时特别好用。
五个关键字段,一个都不能放过
EXPLAIN 输出有十几个字段,但说实话,日常优化我只看五个:type、key、rows、Extra,还有一个容易被忽略的 possible_keys。下面我一个一个讲。
1. type:访问方式的「段位」
这个字段决定了 MySQL 是怎么找到你要的数据的。从快到慢,大致排个序:
| type 值 | 含义 | 优化建议 |
|---|---|---|
| system | 表只有一行(系统表) | 不用管,已经最优了 |
| const | 通过主键或唯一索引等值查找 | 理想状态,保持 |
| eq_ref | 联表时,被驱动表走主键/唯一索引 | 联表查询的理想状态 |
| ref | 普通索引等值匹配 | 还不错,但注意区分度 |
| range | 索引范围扫描(>、<、BETWEEN、IN) | 可接受,但别扫太多 |
| index | 扫描整个索引树 | 比全表好一点,但还是要优化 |
| ALL | 全表扫描 | 红色警报,必须优化 |
我个人习惯是:看到 ALL 就亮红灯,看到 index 就黄灯,看到 range 以上才算及格。
2. key:实际用了哪个索引
这个字段告诉你 MySQL 最终选择了哪个索引。有时候 possible_keys 里列了好几个候选,但 key 是 NULL。这说明 MySQL 觉得走索引还不如全表扫。
为什么会这样?我举个例子:
EXPLAIN SELECT * FROM users WHERE gender = '男';
如果 gender 上有索引,但男女比例接近 1:1,MySQL 会认为扫索引再回表的成本比全表扫描还高。这时候 key 就是 NULL。
遇到这种情况,我的建议是:要么别建这个索引,要么改成覆盖索引。
3. rows:MySQL 预估要扫多少行
这个字段是个预估值,不是精确值。但它非常能说明问题。你想想看,如果 rows 显示 100 万,就算 type 是 ref,查询也快不到哪去。
我一般拿 rows 和实际返回的行数做对比。如果 rows 是 10 万,实际只返回 10 行,说明索引过滤性不够好。这时候就要考虑联合索引或者调整查询条件。
4. Extra:藏着最多「坑」的字段
这个字段里的信息量最大,也最容易被人忽略。我挑几个最常见的来说:
- Using index:好现象,表示查询只用索引就搞定了,不用回表。这叫覆盖索引。
- Using where:中性,表示在存储引擎层拿到数据后,还要在 Server 层过滤。
- Using index condition:MySQL 5.6+ 的索引下推优化,比 Using where 好一点。
- Using filesort:危险信号,表示排序没走索引,在内存或磁盘里排。数据量大时非常慢。
- Using temporary:红色警报,表示用了临时表。常见于 GROUP BY 或 DISTINCT 没走索引。
实战:一个完整的分析流程
光讲理论没意思,我们拿一个真实场景走一遍。
假设有个订单表,结构大概是这样:
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
order_no VARCHAR(32),
status TINYINT,
created_at DATETIME,
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at)
);
现在有个查询:
EXPLAIN SELECT * FROM orders
WHERE user_id = 123
ORDER BY created_at DESC
LIMIT 10;
执行计划可能长这样:
| 字段 | 值 |
|---|---|
| type | ref |
| possible_keys | idx_user_id, idx_created_at |
| key | idx_user_id |
| rows | 5000 |
| Extra | Using filesort |
你看,type 是 ref,key 走了 user_id 索引,看起来还行。但 Extra 里有个 Using filesort,说明排序没走索引。为什么?因为 MySQL 只能用一个索引,它选了 idx_user_id 来过滤,排序就只能自己来了。
怎么优化?建一个联合索引:
ALTER TABLE orders ADD INDEX idx_user_created (user_id, created_at);
再跑一次 EXPLAIN,Extra 就变成 Using index condition 了,filesort 消失,rows 也降到了几十行。
执行计划分析的知识体系
下面这张图是我自己总结的分析流程,每次排查慢查询我都按这个来:
几个容易踩的坑
最后分享几个我这些年踩过的坑,你遇到了可以少走弯路:
- 别只看 type:type 是 ref 不代表快,还要看 rows 和 Extra。我见过 type=ref 但 rows=50 万的查询,一样慢。
- possible_keys 有值不代表走了索引:key 为 NULL 时,说明 MySQL 评估后认为走索引不如全表扫。这时候别硬加索引提示(FORCE INDEX),先看看能不能改查询。
- Using filesort 不一定是排序数据量大:有时候只是索引顺序和 ORDER BY 不匹配。调整索引顺序就能解决。
- rows 是预估值:别太当真,但趋势很有参考价值。优化前后 rows 从 10 万降到 100,效果就很明显。