3. SQL执行计划分析:用EXPLAIN读懂你的查询

说实话,我见过太多开发同学,SQL写完了就跑,根本不关心它怎么执行。直到线上慢查询报警,才慌慌张张来看执行计划。我个人习惯是——写任何SQL之前,先跑一遍EXPLAIN。这就像开车前看一眼仪表盘,花不了几秒钟,但能避免大麻烦。

今天我们就来聊聊EXPLAIN输出的几个关键字段:typekeyrowsExtra。搞懂它们,你就能一眼看穿SQL的性能瓶颈。

3.1 先跑一个EXPLAIN看看

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

EXPLAIN SELECT * FROM users WHERE age > 30;

MySQL会返回一张表,里面每一行对应一个查询步骤。咱们重点看这四个列:

列名 含义 重点关注
type 访问类型 至少要达到range,最好到ref或const
key 实际使用的索引 NULL表示没用到索引,危险
rows 预估扫描行数 越小越好,偏差太大要考虑统计信息
Extra 额外信息 出现Using filesort、Using temporary要警惕

3.2 type:访问类型,性能的晴雨表

type字段告诉你MySQL是怎么找到数据的。从好到差,大致是这么个顺序:

  • system > const > eq_ref > ref > range > index > ALL

我来一个个说清楚。

const:最快,没有之一

按主键或唯一索引查一行数据。比如:

EXPLAIN SELECT * FROM users WHERE id = 1;

type显示const,说明MySQL直接定位到那一行,几乎不花时间。

eq_ref:多表连接时的好信号

出现在join查询中,驱动表用主键或唯一索引去匹配被驱动表。我在项目中优化过一个订单查询,把type从ALL提到eq_ref,查询时间从3秒降到了0.01秒。

ref:普通索引查找

不是唯一索引,可能匹配到多行。比如:

EXPLAIN SELECT * FROM users WHERE age = 25;  -- age上有普通索引

range:索引范围扫描

用了><BETWEENIN这些操作符。我个人认为,range是性能底线——如果连range都达不到,那就要好好反思了。

index:全索引扫描

遍历整个索引树。比全表扫描好一点,但数据量大时依然很慢。

ALL:全表扫描,性能杀手

嗯,这个不用多解释。线上出现ALL,基本就是慢查询预备役。

我的经验: 优化SQL时,我第一个看的就是type。如果出现ALL或index,我会立刻停下来,思考怎么加索引或改写SQL。

3.3 key:到底用了哪个索引

key列显示MySQL实际选中的索引。有时候你建了索引,但MySQL没用它,这时候就要看possible_keyskey的差异。

举个例子:

EXPLAIN SELECT * FROM users WHERE name LIKE '%张三%';

即使你在name上建了索引,因为用了前导通配符%,MySQL可能还是不用索引。key显示NULL,possible_keys也空着——说明这个查询没法走索引。

小技巧: 如果possible_keys有值但key是NULL,说明MySQL觉得用索引还不如全表扫描快。这时候可以试试FORCE INDEX强制走索引,但更推荐的是优化SQL本身。

3.4 rows:预估扫描行数

rows是MySQL估算的需要扫描的行数。注意是估算,不是精确值。我见过有人看到rows=1000就放心了,结果实际扫描了100万行——因为统计信息过期了。

为什么会这样?MySQL的统计信息不是实时更新的。如果你频繁插入删除数据,最好定期跑一下ANALYZE TABLE

ANALYZE TABLE users;

我曾经在一个日活百万的系统上排查慢查询,发现rows显示5000,但实际执行要扫50万行。跑完ANALYZE后,rows变成了48万,优化器终于选对了索引。

注意: rows只是参考值,不要100%相信它。但如果你看到rows是几百万甚至上亿,那这个查询大概率有问题。

3.5 Extra:隐藏的坑都在这

Extra列包含很多额外信息,有些是好的,有些是坏的。我挑几个最常见的说:

Using index:好信号

表示查询只用索引就拿到了数据,不用回表。这叫覆盖索引,性能极佳。

EXPLAIN SELECT id, name FROM users WHERE name = '张三';  -- 假设(id, name)是联合索引

Using where:中性

表示在存储引擎层过滤后,还要在Server层再过滤一次。通常是因为索引无法覆盖所有条件。

Using filesort:坏信号

表示MySQL需要额外排序。注意,filesort不是用文件排序,而是在内存或磁盘上排序。数据量大时非常慢。

比如:

EXPLAIN SELECT * FROM users WHERE age > 20 ORDER BY create_time;

如果(age)和(create_time)没有联合索引,就会出现Using filesort。

避坑指南: 我曾经在一个报表查询里看到Using filesort,当时没在意,结果数据量到500万后,这个查询跑了30秒。后来加了一个(age, create_time)的联合索引,filesort消失了,查询降到0.1秒。

Using temporary:严重警告

表示MySQL用了临时表来存储中间结果。通常出现在GROUP BYDISTINCTUNION这些操作中。

比如:

EXPLAIN SELECT age, COUNT(*) FROM users GROUP BY age;

如果age上没有索引,就会出现Using temporary。数据量大时,临时表可能被放到磁盘上,性能急剧下降。

Using index condition:索引下推

这是MySQL 5.6引入的优化。简单说,就是存储引擎层在扫描索引时,直接过滤掉不符合条件的记录,减少回表次数。看到这个,说明你的MySQL版本和索引设计都不错。

3.6 实战:一个完整的分析案例

假设我们有这样一张订单表:

CREATE TABLE orders (
  id INT PRIMARY KEY,
  user_id INT,
  amount DECIMAL(10,2),
  status TINYINT,
  create_time DATETIME,
  INDEX idx_user_id (user_id),
  INDEX idx_create_time (create_time)
);

现在要查「用户ID为100,且金额大于100的订单,按创建时间排序」:

EXPLAIN SELECT * FROM orders 
WHERE user_id = 100 AND amount > 100 
ORDER BY create_time;

执行计划可能长这样:

typeref
keyidx_user_id
rows500
ExtraUsing where; Using filesort

分析一下:

  • type是ref,说明用了user_id索引,不错
  • key是idx_user_id,确实走了这个索引
  • rows=500,预估扫描500行,还行
  • 但Extra里有Using filesort,说明排序没走索引

优化方案:建一个联合索引(user_id, create_time, amount),这样既能过滤又能排序,还能覆盖查询。

ALTER TABLE orders ADD INDEX idx_user_create_amount (user_id, create_time, amount);

再跑EXPLAIN,Extra变成了Using index condition,filesort消失了。这就是优化的魅力。

3.7 总结一下

EXPLAIN是SQL优化的起点。你不需要记住所有字段,但type、key、rows、Extra这四个必须烂熟于心。我每次做SQL评审,第一件事就是看执行计划。说白了,不懂EXPLAIN,就别谈SQL优化

下一章,我们会聊索引失效的常见场景。嗯,那才是真正的「坑王」章节。