慢查询日志分析 —— 网站慢了,第一步该看什么?
大家好,我是一名拥有10年以上经验的DBA老兵。
做这个系列,源于一个朴素的愿望:把踩过的坑、总结的经验系统化输出,希望能帮到刚入行或想进阶的兄弟们。
让我们开始今天的第23天内容。
前两天聊了死锁,今天聊一个更常见的场景——网站突然变慢了。
这时候大多数人的反应是:重启、加缓存、加机器。但一个合格的 DBA 第一件事是——查慢查询日志。
慢查询日志是 MySQL 给你留的"案发现场"。谁慢了?慢在哪里?花了多久?全记在上面。不看日志就动手,属于闭着眼睛修车。
配置:先确保日志开着
很多服务器的慢查询日志默认是关的。第一步确认它开着:
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
关键参数:
| 参数 | 说明 | 推荐值 |
|---|---|---|
slow_query_log |
是否开启 | ON |
slow_query_log_file |
日志文件路径 | 默认在 data 目录 |
long_query_time |
超过多少秒算慢 | 线上 1 或 2,分析时临时设 0 |
log_queries_not_using_indexes |
没走索引的查询也记录 | 非生产和低负载:ON;高负载生产:酌情开 |
log_slow_admin_statements |
慢管理语句也记录(DDL等) | 建议 ON |
长期建议 long_query_time=1,意思是超过 1 秒就算慢。有些团队设 0.5,也可以,但要看你服务器的负载——如果日志量太大,设太短反而淹没了真正的慢查询。
log_queries_not_using_indexes 也要注意代价:如果业务代码里有大量没走索引的小查询,这个开关一开会让你的日志文件瞬间暴涨,IO 开销也跟着上来。
⚠️ 注意:在高并发生产环境开启
log_queries_not_using_indexes前,先评估你的慢查询量。如果业务代码里大量查询都没走索引(哪怕单次只要几十毫秒),这个开关会让日志量暴增 10-100 倍,IO 也会跟着涨。建议先在从库或低峰期试开,观察一段时间再决定是否长期开启。
一句话总结:非生产环境必开,低负载生产建议开,高并发生产开在从库上或只在排查时临时开。
临时调成 0 抓全量是个好技巧:
SET GLOBAL long_query_time = 0;
-- 跑一段时间抓完,记得改回来
SET GLOBAL long_query_time = 1;
但有个风险:如果你忘了改回来,日志会把磁盘撑爆。 建议要么设个定时任务自动恢复:
# 10 分钟后自动恢复
echo "SET GLOBAL long_query_time = 1;" | mysql -u root -p
要么不用 0,设一个极低但安全的值:
SET GLOBAL long_query_time = 0.01; -- 10 毫秒,过滤掉真正的零耗时查询
注意 SET GLOBAL 对已有连接不生效,新连接才生效。
还有一件事容易被忽略:慢查询日志是同步写操作。 MySQL 每执行完一条符合条件的慢查询,必须把日志写进磁盘才继续往下走。所以日志 IO 本身也会影响性能——如果日志盘 IO 撑满了,查询也会被拖慢。
因此日志文件要定期切割。切割时慢日志和全日志一起做,用 move 方式切换,不要直接 truncate:
# 凌晨低峰期,写个脚本统一切割
mv /var/log/mysql/slow.log /var/log/mysql/slow.log.$(date +%Y%m%d)
mv /var/log/mysql/general.log /var/log/mysql/general.log.$(date +%Y%m%d)
mysqladmin -u root -p flush-logs
为什么用 mv + flush-logs 而不是直接 truncate?
- mv 只是改了文件名,MySQL 进程打开的文件描述符依然指向同一个 inode,所以在 flush 之前,日志会继续写入已被改名的旧文件,不会丢失。
- 执行
flush-logs后,MySQL 关闭旧文件句柄、创建新文件。切换过程有极短暂的阻塞窗口,但选在凌晨低峰期做,影响几乎可以忽略。 - 千万不要
cat /dev/null > slow.log或直接rm正在写的日志文件。
用 logrotate 配好 postrotate 脚本也能自动完成切割。
读一条慢查询日志
一条典型的慢查询日志长这样:
# Time: 2026-05-18T10:30:15.123456Z
# User@Host: app_user[app_user] @ db01.example.com [10.0.1.100] Id: 12345
# Query_time: 12.345678 Lock_time: 0.001234 Rows_sent: 10 Rows_examined: 500000
SET timestamp=1747559415;
SELECT o.id, o.amount, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
ORDER BY o.created_at DESC
LIMIT 10;
每一列都有用:
| 字段 | 含义 | 重点关注 |
|---|---|---|
Query_time |
SQL 执行耗时(秒) | 是否超过 long_query_time |
Lock_time |
锁等待时间 | 如果锁时间占比高,可能是锁竞争 |
Rows_sent |
最终返回的行数 | 正常 |
Rows_examined |
扫描了多少行 | 核心指标 —— Rows_examined 远大于 Rows_sent 说明扫描了大量数据 |
上面这个例子:扫描了 50 万行,只返回了 10 行。 50万/10 的比值过于离谱,一眼就可以判断——o.status 没有索引,或者有索引但选择性太差了。
工具:不要手动翻日志
日志文件大了以后(线上经常几 GB),不要 cat 看,用工具。
mysqldumpslow(MySQL 自带)
# 按平均查询时间排序,看前 10 条
mysqldumpslow -t 10 /var/log/mysql/slow.log
# 按执行次数排序
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log
自带工具的好处是哪里都有,不用额外安装。缺点是分析能力有限,只能做简单的聚合。
pt-query-digest(Percona Toolkit)
这是 DBA 的标配。如果只能装一个 MySQL 工具,我选这个。
# 分析慢查询日志,输出一份完整的报告
pt-query-digest /var/log/mysql/slow.log
# 输出到文件慢慢看
pt-query-digest /var/log/mysql/slow.log > slow_report.txt
pt-query-digest 会输出一份按"总耗时"排序的报告,告诉你哪些 SQL 消耗了最多的数据库时间。排名第一的那个,就是你要优化的头号目标。
举个例子,它的输出会告诉你:
# Rank Query ID Response time Calls R/Call V/M Item
# ==== ================== ================ ====== ======= ===== ====
# 1 0xAB12CD34EF56... 1256.3456 45.2% 1234 1.0178 0.03 SELECT orders
# 2 0xCD78EF90AB12... 456.7890 16.4% 567 0.8056 0.10 SELECT users
第1条 SQL 占了 45% 的慢查询总时间。优化它就解决了一半问题。
如果需要自动化集成(比如定时分析、对接告警系统),pt-query-digest 支持 JSON 输出:
pt-query-digest --output json /var/log/mysql/slow.log > slow.json
JSON 格式按 query 类分组,每条包含 fingerprint、metrics(Query_time、Rows_examined 等)、样本 SQL。配合 jq 处理非常方便,比如提取总耗时排名前5的 SQL:
pt-query-digest --output json slow.log | jq '.classes[:5] | .[].sample'
也有 --output json-anon 模式,会去掉样本 SQL 中的具体数据,适合脱敏后传给第三方分析。
一个完整的分步流程
我处理线上慢查询的步骤:
第一步:确认到底有多慢
-- 看一眼当前正在跑的查询
SHOW FULL PROCESSLIST;
-- 看 Time 列:跑了多久了?
第二步:打开慢查询日志,确认是偶发还是持续
tail -100 /var/log/mysql/slow.log
第 2.5 步:看一眼系统负载(容易被跳过但很重要)
top -bn1 | head -5 # CPU 负载
iostat -x 1 3 # 磁盘 IO 是否打满
free -h # 内存是否吃紧
如果 CPU/IO 已经打满,慢查询可能只是"受害者"。先找到真正的资源杀手(通常也是没走索引的全表扫描),别揪着某条 SQL 死磕。
第三步:用 pt-query-digest 找到头号敌人
pt-query-digest /var/log/mysql/slow.log | head -80
第四步:对目标 SQL 做 EXPLAIN
EXPLAIN SELECT ...\G
第五步:做索引优化或 SQL 改写,验证效果
⚠️ 一个常见的误判:慢的不一定是它自己
慢查询日志告诉你"这条 SQL 花了 10 秒",但原因不一定在这条 SQL 本身。
有种常见的场景:这条 SQL 单独拿出来跑只要 100 毫秒,但在线上就是 10 秒。为什么?因为当时系统里还有其他慢查询在吃资源——CPU 打满了、IO 带宽被占完了、内存被挤爆了。
这条 SQL 本身没有变慢,它是被挤慢的。就像是堵车——不是你的车有问题,是整个路口都堵死了。
怎么判断?
-- 看当时系统的负载状态(如果日志里有)
# Schema: db1 # 数据库名
# Query_time: 10.5 Lock_time: 0.001 # SQL 耗时 10 秒
-- 如果同时间段有大量其他慢查询,且这条 SQL 单独 EXPLAIN 走索引、
-- rows 扫描量很小——大概率是被"挤慢"的,不是它自己的锅
取出来单独跑一下就能验证:
# 把这条 SQL 粘出来,在数据库里跑一次
# 如果单独跑很快,回去查同时间段的系统负载
根因可能是:其他全表扫描的 SQL 吃光了 IO、某个定时任务占满了 CPU、备份脚本在跑。这种情况你的优化目标不是这条 SQL,而是把那个真正的"资源杀手"找出来。
所以诊断慢查询时,别只盯着"最慢的"那一两条。结合 SHOW FULL PROCESSLIST 和系统负载(CPU、IO、内存)一起看,才能找到真正的根因。
慢查询日志分析用 AI 有个常见的误区——把整份日志文件丢给 AI。 如果日志几 MB 甚至几百 MB,Token 消耗是天文数字,AI 光读完就超时了。
正确姿势:先用传统工具做聚合,再把样本交给 AI 做深度分析。
正确流程
慢查询日志(.log)
↓
pt-query-digest --output json(聚合排序,输出 JSON)
↓
从 JSON 中提取排名靠前的 SQL 样本(结构化,Token 极省)
↓
用 AI 执行 EXPLAIN 分析索引使用情况
↓
结合索引优化知识给出改索引或改 SQL 的建议
实操:从 pt-query-digest JSON 到 AI 分析
用 JSON 格式输出,比文本报告更适合 AI 处理——结构清晰、没有无关字符、Token 省很多:
pt-query-digest --output json /var/log/mysql/slow.log > slow.json
然后用 jq 提取排名靠前的样本 SQL 和关键指标:
# 提取前 5 条慢查询的样本 SQL 和执行次数
jq '.classes[:5] | .[] | {sql: .sample, calls: .metrics.Query_time.cnt, total_time: .metrics.Query_time.sum}' slow.json
把这条命令的输出直接交给 AI:
帮我分析这条 SQL 为什么慢,建议创建什么索引:
SELECT o.id, o.amount, u.name FROM orders o JOIN users u ON o.user_id = u.id WHERE o.status = 'pending' ORDER BY o.created_at DESC LIMIT 10;先执行 EXPLAIN,结合输出分析索引使用情况。
如果 AI 工具支持 MCP 直连数据库(比如 OpenCode 连接了数据库 MCP),可以让 AI 直接跑 EXPLAIN:
执行 EXPLAIN SELECT ...,看 type、key、rows、Extra 字段,判断有没有走索引,给出优化建议。
结合前面 Day 5-6 讲的索引原理和 Day 24 即将讲的 EXPLAIN 解读,AI 可以帮你把一条慢查询从"知道慢"推进到"知道为什么慢、怎么修"。
面试加餐
Q: long_query_time 设为 0 会有什么后果?
会记录所有查询。这在排查问题时很有用(临时开启),但生产环境不要长期设为 0。MySQL 写日志是有 IO 开销的,所有查询都记会导致日志疯长,甚至把磁盘写满。用完记得关。
Q: Rows_examined 很大但 Rows_sent 很小,一定有问题吗?
不绝对。
SELECT COUNT(*) FROM big_table即使有索引也扫描了全表,但 Rows_sent = 1,这是正常的。但如果是一个 OLTP 查询(比如按用户查订单),Rows_examined 远大于 Rows_sent,基本可以判断索引有问题。
Q: pt-query-digest 和 mysqldumpslow 哪个好?
pt-query-digest 强太多了。它不仅按总耗时排序,还能做指纹聚合(把
WHERE id = ?参数化后归并)、按时间段分析趋势、输出各种维度的统计。mysqldumpslow 就是 MySQL 自带的"能看就行"的版本。pt-query-digest 是 DBA 工具箱里最常用的工具,没有之一。
总结
🎯 面试考点
- 慢查询日志的配置:
slow_query_log=ON、long_query_time、log_queries_not_using_indexes - 日志关键字段:Query_time、Lock_time、Rows_examined、Rows_sent —— 核心看扫描/返回比值
- 分析工具:
mysqldumpslow(自带基本版)、pt-query-digest(DBA 标配) - 排查流程:SHOW PROCESSLIST → 查慢日志 → pt-query-digest 聚合 → EXPLAIN 定位 → 加索引 / 改 SQL
- Rows_examined >> Rows_sent 是索引缺失的典型信号
今天就试一下:登录你的数据库,跑这条 SQL:
SHOW VARIABLES LIKE 'slow_query%';
看一眼你的慢查询日志开了没有。如果没开——今天就把它打开。
SET GLOBAL slow_query_log = ON;
SET GLOBAL log_queries_not_using_indexes = ON;
下期预告:EXPLAIN 执行计划解读 —— 面试必问!
有问题欢迎评论区交流,明天见!