3. SQL执行计划分析:用EXPLAIN读懂你的查询
说实话,我见过太多开发同学,SQL写完了就跑,根本不关心它怎么执行。直到线上慢查询报警,才慌慌张张来看执行计划。我个人习惯是——写任何SQL之前,先跑一遍EXPLAIN。这就像开车前看一眼仪表盘,花不了几秒钟,但能避免大麻烦。
今天我们就来聊聊EXPLAIN输出的几个关键字段:type、key、rows、Extra。搞懂它们,你就能一眼看穿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:索引范围扫描
用了>、<、BETWEEN、IN这些操作符。我个人认为,range是性能底线——如果连range都达不到,那就要好好反思了。
index:全索引扫描
遍历整个索引树。比全表扫描好一点,但数据量大时依然很慢。
ALL:全表扫描,性能杀手
嗯,这个不用多解释。线上出现ALL,基本就是慢查询预备役。
3.3 key:到底用了哪个索引
key列显示MySQL实际选中的索引。有时候你建了索引,但MySQL没用它,这时候就要看possible_keys和key的差异。
举个例子:
EXPLAIN SELECT * FROM users WHERE name LIKE '%张三%';
即使你在name上建了索引,因为用了前导通配符%,MySQL可能还是不用索引。key显示NULL,possible_keys也空着——说明这个查询没法走索引。
3.4 rows:预估扫描行数
rows是MySQL估算的需要扫描的行数。注意是估算,不是精确值。我见过有人看到rows=1000就放心了,结果实际扫描了100万行——因为统计信息过期了。
为什么会这样?MySQL的统计信息不是实时更新的。如果你频繁插入删除数据,最好定期跑一下ANALYZE TABLE:
ANALYZE TABLE users;
我曾经在一个日活百万的系统上排查慢查询,发现rows显示5000,但实际执行要扫50万行。跑完ANALYZE后,rows变成了48万,优化器终于选对了索引。
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 temporary:严重警告
表示MySQL用了临时表来存储中间结果。通常出现在GROUP BY、DISTINCT、UNION这些操作中。
比如:
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;
执行计划可能长这样:
| type | ref |
| key | idx_user_id |
| rows | 500 |
| Extra | Using 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优化。
下一章,我们会聊索引失效的常见场景。嗯,那才是真正的「坑王」章节。