执行计划分析:EXPLAIN命令详解、type列解读、Extra信息含义、慢查询日志配置与分析
说实话,很多开发同学写SQL的时候,心里是没底的。写完一跑,能出结果就行。但性能好不好?会不会把数据库拖垮?没人知道。
我个人的习惯是,任何上线前的SQL,必须过一遍执行计划。这就像开车前看一眼仪表盘,心里才有底。今天我们就来聊聊这个“仪表盘”怎么看。
EXPLAIN命令:你的SQL透视镜
EXPLAIN 是 MySQL 提供的一个神器。你只需要在 SQL 前面加上 EXPLAIN 关键字,它就会告诉你 MySQL 打算怎么执行这条语句。
EXPLAIN SELECT * FROM users WHERE age > 30;
输出结果是一张表,每一行代表一个查询步骤。关键列有这些:
| 列名 | 含义 |
|---|---|
| id | 查询的标识符,id越大越先执行 |
| select_type | 查询类型,比如SIMPLE、PRIMARY、SUBQUERY |
| table | 正在访问哪张表 |
| type | 访问类型,性能优化的核心指标 |
| possible_keys | 可能用到的索引 |
| key | 实际用到的索引 |
| key_len | 索引使用的字节数 |
| ref | 索引的哪一列被使用了 |
| rows | 预估需要扫描的行数 |
| Extra | 额外的补充信息,经常藏着大坑 |
type 和 Extra。这两列如果出现“坏信号”,基本就可以断定这条SQL需要优化了。
type列解读:从全表扫描到常量查找
type 列描述了 MySQL 如何查找数据行。性能从好到差,大致是这样:
| type | 性能 | 说明 |
|---|---|---|
| system | 最好 | 表只有一行,基本见不到 |
| const | 极好 | 通过主键或唯一索引查找,只匹配一行 |
| eq_ref | 很好 | 多表连接时,使用主键或唯一索引 |
| ref | 好 | 使用普通索引,返回多行 |
| range | 一般 | 索引范围扫描,比如 BETWEEN、>、IN |
| index | 较差 | 扫描整个索引树,比全表快一点 |
| ALL | 最差 | 全表扫描,性能杀手 |
我在项目中遇到过好几次,线上慢查询的 type 都是 ALL。有一次查一个订单表,数据量才几十万,但每次查询都要好几秒。一看执行计划,type 是 ALL,rows 显示扫描了 30 万行。加了个索引后,type 变成了 ref,查询时间降到了 10 毫秒以内。
ALL 或 index,而且表数据量很大,这基本就是慢查询的根源。赶紧检查索引吧。
Extra信息含义:隐藏的细节
Extra 列里经常出现一些“暗语”,我来翻译几个最常见的:
- Using index:覆盖索引。查询所需的数据都在索引里,不需要回表。这是好事。
- Using where:在存储引擎层过滤后,还需要在服务层再过滤。通常意味着索引没有完全覆盖查询条件。
- Using temporary:使用了临时表。常见于
GROUP BY和ORDER BY字段不一致时。性能杀手。 - Using filesort:文件排序。无法利用索引排序,MySQL 不得不自己排序。数据量大时很慢。
- Using index condition:索引下推。MySQL 5.6 以后引入的优化,能在索引层面过滤数据,减少回表次数。
嗯,这里要注意。如果看到 Using temporary 和 Using filesort 同时出现,那基本就是“双重暴击”。我曾经优化过一个报表查询,Extra 里这两个都出现了,查询跑了 20 秒。后来通过调整索引顺序,让 GROUP BY 和 ORDER BY 用上了同一个索引,查询时间直接降到了 0.5 秒。
慢查询日志:找到那些“拖后腿”的SQL
执行计划是分析单条SQL的。但线上那么多SQL,你怎么知道哪些是慢的?这就需要慢查询日志了。
配置慢查询日志,通常有两种方式:
方式一:修改配置文件(永久生效)
# 在 my.cnf 或 my.ini 中添加
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 2
log_queries_not_using_indexes = 1
方式二:动态设置(无需重启)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';
SET GLOBAL long_query_time = 2;
SET GLOBAL log_queries_not_using_indexes = 'ON';
参数说明:
long_query_time:超过多少秒算慢查询。我一般设成 1 秒或 2 秒。log_queries_not_using_indexes:记录没有使用索引的查询。这个很有用,能帮你发现“漏网之鱼”。
long_query_time 设成 0,记录所有SQL。这样能全面了解应用的查询模式。但线上环境建议设成 1 或 2,避免日志量过大。
慢查询日志分析:从日志到优化
日志开启后,你会看到类似这样的内容:
# Time: 2025-01-15T10:30:00.123456Z
# User@Host: root[root] @ localhost []
# Query_time: 3.500724 Lock_time: 0.000182 Rows_sent: 100 Rows_examined: 500000
SET timestamp=1736922600;
SELECT * FROM orders WHERE status = 'pending' ORDER BY created_at DESC;
关键信息:
- Query_time:查询耗时,3.5秒,明显是慢查询。
- Rows_examined:扫描了 50 万行,但只返回了 100 行。说明扫描了大量无用数据。
- Rows_sent:实际返回的行数。
拿到这条SQL后,我会做两件事:
- 用 EXPLAIN 分析执行计划,看看 type 和 Extra。
- 检查
status和created_at是否有合适的索引。
我曾经遇到过一个问题,慢查询日志里全是同一个查询。分析后发现,status 字段的区分度太低(只有几个值),导致 MySQL 觉得用索引还不如全表扫描快。后来我改成了 (status, created_at) 的联合索引,查询速度提升了几十倍。
知识体系总览
下面这张图,帮你把本章的核心逻辑串起来:
说白了,执行计划分析就是一条线:发现问题(慢查询日志)→ 定位问题(EXPLAIN)→ 解决问题(索引优化)。这三步走通了,你离SQL优化高手就不远了。