执行计划分析:如何用EXPLAIN命令找到慢查询的根因

说实话,我见过太多开发同学一遇到查询慢,第一反应就是「加索引」。加完发现没效果,又跑来问我。我通常会反问一句:你看过执行计划了吗?大部分人沉默了。

执行计划,说白了就是数据库告诉你「我打算怎么查这张表」。你连数据库的计划都不看,怎么知道它走没走索引?走了哪个索引?扫了多少行?

这一章,我就带你把这个工具彻底用熟。

EXPLAIN 怎么用?

用法极其简单。在你的 SELECT 语句前面加上 EXPLAIN 关键字就行:

EXPLAIN SELECT * FROM orders WHERE order_id = 10086;

MySQL 会返回一张表,里面每一行对应一个查询步骤。嗯,就这么简单。但真正值钱的是怎么读这张表。

小提示: 如果你用的是 MySQL 8.0.18+,还可以试试 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 以上才算及格。

注意: 别以为 type 是 ref 就万事大吉。我曾经接手过一个订单表,type 显示 ref,但 rows 显示扫了 20 万行。为什么?因为索引区分度太差了。所以 type 要和 rows 一起看。

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 没走索引。
避坑指南: 我曾经优化过一个报表查询,Extra 里同时出现 Using temporary 和 Using filesort。查了半小时才发现是 GROUP BY 的字段顺序和索引顺序不一致。调整后查询时间从 12 秒降到了 0.3 秒。

实战:一个完整的分析流程

光讲理论没意思,我们拿一个真实场景走一遍。

假设有个订单表,结构大概是这样:

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 也降到了几十行。

我的习惯: 每次建索引前,我都会先用 EXPLAIN 验证一下。等索引建好,再跑一次确认效果。别嫌麻烦,这一步能省后面很多排查时间。

执行计划分析的知识体系

下面这张图是我自己总结的分析流程,每次排查慢查询我都按这个来:

EXPLAIN 分析流程 EXPLAIN 输出 第一步:看 type 是否 ALL 或 index 第二步:看 key 是否为 NULL 第三步:看 rows 是否过大 第四步:看 Extra 中的危险信号 定位瓶颈 → 制定优化方案 按这个流程走一遍,90% 的慢查询都能找到根因

几个容易踩的坑

最后分享几个我这些年踩过的坑,你遇到了可以少走弯路:

  • 别只看 type:type 是 ref 不代表快,还要看 rows 和 Extra。我见过 type=ref 但 rows=50 万的查询,一样慢。
  • possible_keys 有值不代表走了索引:key 为 NULL 时,说明 MySQL 评估后认为走索引不如全表扫。这时候别硬加索引提示(FORCE INDEX),先看看能不能改查询。
  • Using filesort 不一定是排序数据量大:有时候只是索引顺序和 ORDER BY 不匹配。调整索引顺序就能解决。
  • rows 是预估值:别太当真,但趋势很有参考价值。优化前后 rows 从 10 万降到 100,效果就很明显。
一句话总结: EXPLAIN 是数据库优化的「听诊器」。别凭感觉加索引,先听一听数据库到底在干什么。