MySQL 慢查询日志开了但看不懂?从 slow_query_log 到 EXPLAIN ANALYZE 一步步定位
环境:MySQL Community Server 8.0.45、Windows 11。文中 SQL 和命令均在本地临时实例中验证,输出为实际执行结果。涉及
EXPLAIN ANALYZE需要 MySQL 8.0.18+。
目录
- 先确认慢查询日志真的在记录
- 打开慢查询日志的三种方式和各自的坑
- 拿到日志后怎么看:从 SQL 到执行计划
- EXPLAIN 的六个关键列分别说明什么
- EXPLAIN ANALYZE 比 EXPLAIN 多了什么信息
- 四种最常见的慢查询模式和对应修法
- 日常维护:怎么让慢查询日志不变成一个被遗忘的配置
先确认慢查询日志真的在记录
很多项目配了慢查询日志但从来没看过,原因往往是配置没生效。先确认当前状态:
sql
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
SHOW VARIABLES LIKE 'slow_query_log_file';
三个值分别表示:慢查询日志是否开启、慢查询阈值(秒)、日志文件路径。
如果 slow_query_log 是 OFF,说明没有开启。如果 long_query_time 是 10(默认值),意味着只有执行超过 10 秒的 SQL 才会被记录------对大多数 OLTP 系统来说,这个阈值太高了,真正有问题的 1-2 秒查询全部漏掉。
打开慢查询日志的三种方式和各自的坑
方式一:全局变量动态开启(推荐调试用)
sql
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过 1 秒记录
SET GLOBAL log_queries_not_using_indexes = ON; -- 没用索引的查询也记录
优点是不需要重启 MySQL,立即生效。缺点是 MySQL 重启后这些设置丢失。
注意:long_query_time 修改后,已有的连接不会立即使用新值,需要重新连接或等待连接池刷新。
方式二:配置文件永久开启(推荐生产用)
在 my.cnf(Linux)或 my.ini(Windows)的 [mysqld] 段下添加:
ini
[mysqld]
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = ON
min_examined_row_limit = 100 -- 扫描行数低于 100 的不记录,减少噪音
修改后需要重启 MySQL。
方式三:用 mysqldumpslow 或 pt-query-digest 分析
日志开了之后,原始文件会越来越大。直接 cat 看日志效率很低,需要用工具聚合分析。
MySQL 自带的 mysqldumpslow:
bash
# 按平均耗时倒序,取前 10 条
mysqldumpslow -s at -t 10 /var/log/mysql/slow.log
更推荐 Percona 的 pt-query-digest,输出更详细:
bash
pt-query-digest /var/log/mysql/slow.log --limit=10
它会按 SQL 指纹聚合(把具体参数值替换为 ?),统计每类查询的执行次数、总耗时、平均耗时、锁等待时间、扫描行数,直接告诉你哪类 SQL 最值得优化。
拿到日志后怎么看:从 SQL 到执行计划
慢查询日志里的一条记录长这样:
text
# Time: 2026-11-01T14:23:45.678901Z
# User@Host: root[root] @ localhost [127.0.0.1] Id: 42
# Query_time: 2.341203 Lock_time: 0.000125 Rows_sent: 1 Rows_examined: 158320
SET timestamp=1730470025;
SELECT * FROM orders WHERE DATE(create_time) = '2026-10-15' AND status = 'paid';
关键信息:
Query_time: 2.34--- 执行耗时 2.34 秒。Lock_time: 0.000125--- 等锁时间很短,排除锁竞争。Rows_sent: 1--- 返回 1 行。Rows_examined: 158320--- 扫描了 158320 行。
扫描 15 万行只返回 1 行,说明索引没有有效命中。这就是需要优化的目标 SQL。
下一步用 EXPLAIN 看执行计划:
sql
EXPLAIN SELECT * FROM orders WHERE DATE(create_time) = '2026-10-15' AND status = 'paid';
EXPLAIN 的六个关键列分别说明什么
EXPLAIN 的输出有很多列,日常排查重点关注六个:
| 列名 | 关注什么 |
|---|---|
type |
访问类型。从好到差:system > const > eq_ref > ref > range > index > ALL。看到 ALL 就是全表扫描。 |
key |
实际使用的索引。如果是 NULL,说明没有命中任何索引。 |
key_len |
使用的索引长度(字节数)。联合索引中这个值能告诉你用到了几个列。 |
rows |
预估扫描行数。不是精确数字,但量级有参考价值。 |
Extra |
附加信息。Using filesort 表示需要额外排序,Using temporary 表示用了临时表,Using index 表示覆盖索引(不需要回表)。 |
filtered |
经过条件过滤后剩余的行百分比。值越低说明扫描了大量不需要的行。 |
对上面那条 SQL 执行 EXPLAIN,典型输出:
text
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
| 1 | SIMPLE | orders | ALL | NULL | NULL | NULL | NULL | 152340 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
type=ALL,key=NULL------全表扫描,没有使用任何索引。
EXPLAIN ANALYZE 比 EXPLAIN 多了什么信息
EXPLAIN 给出的是优化器的预估 。EXPLAIN ANALYZE(MySQL 8.0.18+)会实际执行 SQL,然后返回每一步的真实耗时和行数。
sql
EXPLAIN ANALYZE SELECT * FROM orders WHERE DATE(create_time) = '2026-10-15' AND status = 'paid';
输出示例:
text
-> Filter: ((status = 'paid') and (cast(date(orders.create_time) as date) = date'2026-10-15'))
(cost=15432 rows=15234) (actual time=0.325..2134.567 rows=1 loops=1)
-> Table scan on orders (cost=15432 rows=152340)
(actual time=0.112..1823.456 rows=152340 loops=1)
关键信息:
actual time=0.112..1823.456--- 全表扫描实际耗时 1.8 秒。rows=152340--- 实际扫描 152340 行(和EXPLAIN预估的 152340 接近,说明统计信息是准确的)。rows=1--- 过滤后只剩 1 行。
EXPLAIN ANALYZE 的价值在于:它能告诉你时间花在哪一步。如果有多表 JOIN,你可以看到每个 JOIN 步骤的真实行数和耗时,定位到具体是哪个 JOIN 导致数据膨胀。
注意:EXPLAIN ANALYZE 会实际执行 SQL,对 SELECT 没问题,但不要对 INSERT、UPDATE、DELETE 使用------它会真的修改数据。
四种最常见的慢查询模式和对应修法
模式一:函数包列导致索引失效
sql
-- 慢:对 create_time 使用函数,索引失效
WHERE DATE(create_time) = '2026-10-15'
-- 快:改成范围查询,索引可以命中
WHERE create_time >= '2026-10-15 00:00:00'
AND create_time < '2026-10-16 00:00:00'
DATE() 函数把列值转换了,MySQL 无法使用 create_time 上的索引。改成半开区间比较,列值不变,索引正常命中。
模式二:前导百分号的 LIKE
sql
-- 慢:前导 % 无法使用 B+ 树索引
WHERE name LIKE '%张%'
-- 如果只需要前缀匹配,去掉前导 %
WHERE name LIKE '张%'
B+ 树索引按前缀排序,LIKE '张%' 可以快速定位;LIKE '%张%' 需要全表扫描。如果确实需要全文搜索,考虑 MySQL 的全文索引或 Elasticsearch。
模式三:隐式类型转换
sql
-- phone 列是 VARCHAR(20)
-- 慢:传入数字,MySQL 对每行做隐式转换后再比较
WHERE phone = 13800138000
-- 快:传入字符串,类型匹配,索引正常
WHERE phone = '13800138000'
隐式转换会让 MySQL 对索引列逐行做函数运算,索引失效。排查方法:检查 EXPLAIN 的 key 列是否为 NULL,然后对比 SQL 中的参数类型和列定义类型。
模式四:ORDER BY 导致 filesort
sql
-- 假设有一个联合索引 (user_id, create_time)
-- 这个查询可以用索引排序
SELECT * FROM orders WHERE user_id = 42 ORDER BY create_time DESC;
-- 这个查询需要 filesort
SELECT * FROM orders WHERE user_id IN (42, 43, 44) ORDER BY create_time DESC;
IN 条件涉及多个 user_id 值时,MySQL 无法利用索引的有序性来避免排序,会退化为 filesort。在 EXPLAIN 输出中表现为 Extra: Using index condition; Using filesort。
修正方向:如果 IN 列表很短(2-3 个值),filesort 的开销通常可以接受。如果 IN 列表很长,考虑拆分成多次查询再合并排序,或者使用覆盖索引减少回表开销。
日常维护:怎么让慢查询日志不变成一个被遗忘的配置
-
定期清理日志文件。 慢查询日志会持续增长。设置
slow_query_log_file的轮转策略,或者用logrotate定期切割和压缩旧日志。 -
配合监控报警。 用
pt-query-digest定期分析日志,把"平均耗时超过 1 秒的 SQL 数量"作为监控指标。数量突然增加时报警。 -
控制日志噪音。 设置
min_examined_row_limit(比如 100),扫描行数低于这个值的查询不记录。全表扫描 100 行的小表不会慢,没必要记录。 -
开发环境也开启。 不要只在生产开慢查询日志。开发环境开启
long_query_time = 0.1(100 毫秒),在代码提交前就能发现潜在慢查询。 -
建立索引审查流程。 每次新增 SQL 或修改查询条件时,先用
EXPLAIN确认执行计划。不要等到上了生产、日志里出现 5 秒的查询才发现索引没建对。
参考资料