为什么慢查询日志是优化数据库的第一抓手
很多站长遇到网站变慢,第一反应是加内存、换 CPU,或者盲目给表加索引。但在动手之前,你必须先知道到底是哪条 SQL 在拖后腿。MySQL 自带的慢查询日志(slow query log)就是回答这个问题最直接的工具:它把执行时间超过阈值的语句原样记录下来,配合分析工具,你能在几分钟内定位到真正的性能瓶颈,而不是靠猜。
这篇教程面向自建服务器、跑 WordPress 或自研 PHP 应用的站长,从开启慢日志、设置合理阈值,到用 pt-query-digest 做聚合分析,再到把慢 SQL 变成可执行的优化动作,走一遍完整闭环。全过程基于 MySQL 5.7/8.0,命令可直接复制。
第一步:确认慢查询日志是否已开启
先看当前状态。登录 MySQL 执行:
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'slow_query_log_file';
SHOW VARIABLES LIKE 'long_query_time';
SHOW VARIABLES LIKE 'log_queries_not_using_indexes';
如果 slow_query_log 是 OFF,说明还没开。默认 long_query_time 是 10 秒,对现代服务器来说太宽松了——10 秒的查询早就把连接池打爆了,实际上 1 秒甚至 0.5 秒就该记录。
把关键参数写进配置文件 /etc/my.cnf 或 /etc/mysql/my.cnf 的 [mysqld] 段:
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 0
min_examined_row_limit = 100
几个参数值得解释:
long_query_time = 1:超过 1 秒的语句才记录。线上高并发站点可以设 0.5。log_queries_not_using_indexes = 0:默认不要打开。如果打开,所有没走索引的查询都会记,慢日志会瞬间膨胀到几百 MB,反而没法分析。需要时临时开。min_examined_row_limit = 100:扫描行数少于 100 的不记录,过滤掉全表扫描小表的噪声。
修改后无需重启数据库,动态变量可以热加载:
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;
FLUSH LOGS;
注意 long_query_time 是会话级变量,SET GLOBAL 只对之后新建的连接生效,当前会话仍用旧值。要立即对新连接生效,FLUSH LOGS 会轮转日志文件让配置落地。
第二步:读懂慢日志的原始格式
打开慢日志文件看一条真实记录:
tail -n 40 /var/log/mysql/slow.log
典型内容长这样:
# Time: 2026-10-10T09:12:33.114233Z
# User@Host: wpuser[wpuser] @ localhost [] Id: 1284
# Query_time: 3.482119 Lock_time: 0.000211 Rows_sent: 12 Rows_examined: 480233
SET timestamp=1760089953;
SELECT p.ID, p.post_title FROM wp_posts p
JOIN wp_postmeta m ON p.ID = m.post_id
WHERE m.meta_key = 'view_count' ORDER BY m.meta_value+0 DESC LIMIT 12;
关键字段的读法:
- Query_time:这条语句实际耗时 3.48 秒,是我们要干掉的目标。
- Lock_time:等锁时间 0.0002 秒,可以忽略。如果 Lock_time 和 Query_time 接近,说明是表锁/行锁竞争问题,而不是索引问题。
- Rows_sent / Rows_examined:返回 12 行却扫描了 48 万行,比例严重失衡——这就是典型的"全表扫描 + 排序"坏味道。健康的长查询应该是 Rows_examined 接近 Rows_sent。
还有两点容易漏掉:
SET timestamp=...之后的那行才是真正的 SQL。慢日志不会记录参数值之外的东西,如果语句是预处理语句,会出现Prepare、Execute等多行。- 日志里的时间是 UTC(带
Z后缀)。排查"某时段慢"的问题时,记得把业务时间换算成 UTC 再对齐。
第三步:用 pt-query-digest 做聚合分析
手工看慢日志只能看到个别句子,真正有用的是聚合统计——哪类 SQL 累计耗时最多。Percona Toolkit 里的 pt-query-digest 是干这个的标准工具。
# Debian/Ubuntu
apt-get install -y percona-toolkit
# 或者下载官方 deb 包安装
# 分析慢日志,输出到报告
pt-query-digest /var/log/mysql/slow.log > /tmp/slow_report.txt
head -n 80 /tmp/slow_report.txt
报告顶部有几块内容必须看懂:
- Overall:total 语句数、unique 指纹数、QPS、总耗时。如果 total 是 20 万条但去重后只有 30 个指纹,说明是少数几类 SQL 被反复执行,优化这 30 条收益极大。
- Profile 排名表:按"总耗时占比"排序,头部第一名往往占 40% 以上。这就是你的第一优先级。
- 每条指纹的 pct、total、avg、p95:p95 比 avg 更有价值,它能暴露"偶尔卡一下"的长尾查询。
一个常见误区是只盯平均耗时。假设某查询平均 0.3 秒,看着还行,但如果它每秒执行 200 次、p95 高达 5 秒,那它锁住的连接数足以拖垮整站。分析时永远先看总耗时占比,再看单次耗时。
还可以按用户、按时间窗口切分:
# 只看某个时段的慢查询
pt-query-digest --since '2026-10-10 09:00:00' \
--until '2026-10-10 10:00:00' /var/log/mysql/slow.log
# 只统计没有走索引的语句
pt-query-digest --filter '$event->{No_index_used}' /var/log/mysql/slow.log
第四步:把报告变成具体动作
拿到 Top 慢 SQL 后,用 EXPLAIN 看执行计划,常见三类问题和对应解法:
1. 索引缺失或选错
EXPLAIN SELECT p.ID FROM wp_posts p
JOIN wp_postmeta m ON p.ID = m.post_id
WHERE m.meta_key='view_count';
如果 type 是 ALL(全表扫描)或 index(扫整个索引),关键列 Extra 出现 Using filesort,就要考虑加联合索引。例如上面这条,给 wp_postmeta(meta_key, post_id) 建联合索引,能让过滤直接命中。
2. 隐式类型转换导致索引失效
如果 meta_value 是 varchar 却拿来和数字比较(meta_value > 100),MySQL 会把列转成数字,索引直接废掉。解决方式是保证比较两侧类型一致,或改用专门的数值列。
3. 排序 + 分页的深翻页问题
ORDER BY ... LIMIT 10000, 20 会先排序再丢弃前 10000 行。优化手段是用覆盖索引让排序在索引上完成,或者用"游标分页"(记录上一页最后一条的 ID,下一页用 WHERE id > last_id)替代 offset 翻页。
改完索引后,务必回头再跑一次慢日志验证。优化是否有效,以慢日志里该指纹的出现次数和总耗时是否下降为准,而不是凭感觉。
第五步:长期机制与几个坑
- 日志轮转:慢日志会持续增长。用 logrotate 配置按天切割,避免撑爆磁盘。在
/etc/logrotate.d/mysql-slow里指定daily、rotate 7、copytruncate。用copytruncate而不是create,是因为 mysqld 一直持有文件句柄,直接 move 掉它还会往旧 inode 写。 - pt-query-digest 采样:日志太大时可以加
--sample 0.1只分析 10% 的样本,速度快很多,趋势判断依然准确。 - 不要在生产高峰开 log_queries_not_using_indexes:会写爆磁盘 IO。
- 慢日志本身也有开销:每条超阈值的语句都要写盘。阈值设得过低(比如 0.1 秒)在高 QPS 下会明显增加 IO 压力。根据站点量级在 0.5 到 2 秒之间取值。
把慢查询日志纳入日常巡检——每周导出一次 Top 10 慢 SQL,配合监控看趋势——你会发现绝大多数数据库性能问题,在爆发成故障之前就已经在慢日志里露出苗头了。这才是运维该有的主动性。
相关阅读:MySQL 索引失效的几种经典场景、InnoDB 缓冲池命中率监控、以及用监控工具把数据库指标做成仪表盘,都是把慢日志分析能力放大成体系的好补充。