慢查询日志分析:从Workbench到索引优化
发布日期: 2026/08/12 阅读总量: 1

下午2点30分,商家后台“订单列表”接口突然大面积超时,监控告警显示MySQL CPU跑到99%。打开MySQL Workbench,先看Performance Dashboard,InnoDB行锁等待曲线拉满,但Dashboard只告诉我“数据库有问题”,没说哪条SQL是凶手。

我打开慢查询日志,mysql-slow.log已经涨到800MB,grep user_id后刷出几千行结果。这一刻我意识到:慢查询日志不是没开,是开了之后没人会用。

问题:慢查询日志为什么会变成“数据垃圾”

慢查询日志本身没有做聚合,每条SQL按事件顺序写进文件。业务量一大,文件膨胀极快。我那个800MB的日志,实际有效的语句只有几百条,剩下全是重复SQL和没走索引的小查询。

这种情况下,谁用编辑器去翻日志谁傻。正确做法是先用工具聚合,再人工确认。

三个分析工具,我为什么最后用 pt-query-digest

工具依赖功能适合场景缺点
mysqldumpslowMySQL自带TOP N聚合快速看总量不输出报告,维度少
pt-query-digestPercona Toolkit指纹聚合、离群点、HTML报告深度分析需要安装
MySQL Workbench自带、需Performance Schema可视化验证看趋势不能直接解析慢日志文件

三个工具不冲突。我的习惯:Workbench 看症状,pt-query-digest 挖日志,mysqldumpslow 应急。下面按这个顺序走完一次实战。

第一步:把慢查询日志打开

MySQL 8.0.35,先在命令行动态开启,不重启实例:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 'ON';
SET GLOBAL min_examined_row_limit = 100;

long_query_time 单位是秒,支持小数。1 表示超过 1 秒的语句进日志。min_examined_row_limit 一定要设,否则没走索引但扫描行数很少的SQL也会被 log_queries_not_using_indexes 抓进来,日志会爆炸。

SET GLOBAL 只改内存,重启失效。要永久生效,改 my.cnf:

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
min_examined_row_limit = 100

改完重启 MySQL,确认状态:

mysql -uroot -p -e "SHOW VARIABLES LIKE 'slow_query_log%';"

第二步:用 mysqldumpslow 快速看清全貌

日志跑了一天后,先用 MySQL 自带工具看 TOP 10:

mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log

-s t 按总执行时间排序,-t 10 取前10条。输出长这样:

Count: 512  Time=2.21s (1131s)  Lock=0.00s (0s)  Rows_sent=10.0 (5120)
SELECT * FROM orders WHERE user_id = N ORDER BY created_at DESC LIMIT N

mysqldumpslow 把具体值替换成了 N,方便聚合。它足够快,适合应急。但没法看 SQL 指纹,也没法生成报告。

第三步:用 pt-query-digest 把日志变成报告

Ubuntu 24.04 上装 Percona Toolkit 3.1.0:

apt update && apt install -y percona-toolkit

生成文本报告:

pt-query-digest /var/log/mysql/mysql-slow.log --limit 20 > /tmp/slowlog-$(date +%F).txt

生成 HTML 报告,方便发给其他同事:

pt-query-digest --limit=20 --outliers --output=html /var/log/mysql/mysql-slow.log > /tmp/slowlog-$(date +%F).html

--outliers 会单独列执行时间明显偏离平均值的语句,这种SQL往往比普通慢查询更值得处理。

写一个定时分析脚本,每天凌晨4点跑一次:

#!/bin/bash
# /usr/local/bin/analyze-slowlog.sh
SLOW_LOG="/var/log/mysql/mysql-slow.log"
REPORT_DIR="/var/reports/slowlog"
mkdir -p "$REPORT_DIR"
pt-query-digest --limit=20 --outliers \
  --output=html "$SLOW_LOG" \
  > "$REPORT_DIR/report-$(date +%F).html"

加 crontab:

0 4 * * * /usr/local/bin/analyze-slowlog.sh

第四步:用 Workbench 定位到具体时段

pt-query-digest 给出了SQL文本,但没告诉我是哪个时间段发的。打开 Workbench 8.0.36 的 Performance Dashboard,选到下午2点到3点,看到 CPU 和 InnoDB 行锁曲线在那个时段拉满。这能确认慢查询和业务高峰对应。

在 Workbench 里执行这条 SQL,查 performance_schema 的语句摘要:

SELECT
  SCHEMA_NAME,
  DIGEST_TEXT,
  COUNT_STAR,
  ROUND(AVG_TIMER_WAIT / 1e12, 2) AS avg_sec,
  ROUND(SUM_TIMER_WAIT / 1e12, 2) AS total_sec,
  SUM_ROWS_EXAMINED,
  SUM_ROWS_SENT,
  SUM_ROWS_EXAMINED / SUM_ROWS_SENT AS examined_sent_ratio
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME = 'shop'
ORDER BY total_sec DESC
LIMIT 10;

这里有个关键点:TIMER_WAIT 的单位是皮秒,1 秒等于 1e12 皮秒。不除以 1e12,查出来的 avg_sec 会大得离谱。这个坑我踩过。

第五步:把 SQL 拿出来做索引优化

慢日志里最扎眼的是这条:

SELECT * FROM orders
WHERE user_id = 88231
ORDER BY created_at DESC
LIMIT 10;

orders 表 120 万行,历史遗留问题,除了主键没有任何索引。EXPLAIN 看得很清楚:

EXPLAIN SELECT * FROM orders WHERE user_id=88231 ORDER BY created_at DESC LIMIT 10;

结果:type=ALL,rows=1,206,500。这就是全表扫描。

加复合索引:

ALTER TABLE orders
  ADD INDEX idx_user_id_created_at (user_id, created_at);

加了索引后再 EXPLAIN:type=ref,key=idx_user_id_created_at,rows=47。优化完成。

效果数据:优化前后差别有多大

用 ab 压测接口,每轮 1000 个请求,并发 50,跑三次取平均值:

ab -n 1000 -c 50 -H "Authorization: Bearer $TOKEN" \
  http://localhost:8080/api/orders?user_id=88231
指标优化前优化后降幅
单条 SQL 执行时间2.31 s45 ms98%
接口 P95 耗时2100 ms89 ms95.8%
MySQL CPU 使用率85%12%-73 pct
慢查询条数(24h)3210899.8%
typeALLref-
rows1,206,5004799.9%

慢查询日志里还剩 8 条,是其他SQL,不是这条。

为什么提升这么大?不是所有 SQL 都能从 2.3s 降到 45ms。这次是典型的全表扫描 + 排序,复合索引直接覆盖了 WHERE 和 ORDER BY,MySQL 不用再生成临时表排序。

避坑:这 5 个坑我全踩过

坑1:SET GLOBAL 在当前 session 不生效

在 Workbench 里执行 SET GLOBAL slow_query_log='ON',然后同一个窗口查 SHOW VARIABLES LIKE 'slow_query_log',显示还是 OFF。因为 SET GLOBAL 改的是全局变量,当前 session 的变量值不会刷新。要查全局用 SHOW GLOBAL VARIABLES,或者重开一个连接确认。

坑2:log_queries_not_using_indexes 把日志撑爆

这个参数的本意是抓没走索引的查询,但 MySQL 判断“没走索引”时,不看扫描行数。结果大量扫描 10 行以内的小查询全被记录。我见过一晚上日志涨到 20G,Workbench 打开直接卡死。解决:必须同时设置 min_examined_row_limit,我设的是 100。这样只有“没走索引并且扫描行数大于等于 100”的查询才记录。

坑3:Workbench 的性能报告依赖 Performance Schema

Workbench 的 Performance Dashboard 和 Performance Reports 读的是 performance_schema 表。如果 MySQL 实例是低配,performance_schema 默认可能没开。用 SHOW VARIABLES LIKE 'performance_schema'; 确认。开启后还要注意 events_statements_summary_by_digest 是累计值,不是按小时保留的。要查最近时间,得查 events_statements_history_long。

坑4:慢日志时间和 Workbench 图表对不上

慢查询日志写的是本地时间,Performance Schema 的 TIMER_WAIT 是服务器启动后的皮秒数,events_statements_summary_by_digest 只有累计值,没有具体发生时间。Workbench 的图是秒级聚合,慢日志按天记录。不要把两个时间精确到秒对齐,能对上小时级别就不错了。要精确定位,用 events_statements_history_long 查具体时间窗口。

坑5:mysqldumpslow 和 pt-query-digest 数字不一致

同一个慢日志,两个工具出来的 Count、Time 完全不同。原因:pt-query-digest 会识别 SQL 指纹,把变量替换掉再做聚合,而且会把事务、连接错误也算进统计;mysqldumpslow 的聚合简单很多。不要因为数字不一样就说工具不准,统一用一个工具做长期趋势对比就够了。

还有个隐藏坑:如果 log_output=TABLE,慢查询会写到 mysql.slow_log 表,不写文件。pt-query-digest 只能读文件,读不到表。查日志前先确认 SHOW VARIABLES LIKE 'log_output';,是 FILE 才用文件工具。

慢查询日志分析不是跑一条命令就完事。工具只能告诉你哪条 SQL 慢,真正的问题是为什么慢。看 rows、看 type、看索引,把 SQL 和业务场景连起来,才把日志变成价值。