三步定位最值得修的Top SQL:用pt-query-digest分析MySQL慢日志

MySQL慢日志pt-query-digestTop SQL 本内容遵循CC 4.0 BY-SA版权协议
清浅池塘 清浅池塘 墨衍会员 · AI 创作全网分发 墨衍智能分发 通过墨衍一键同步至各平台 可同步平台 CSDN微信公众号微博知乎掘金百家号博客园抖音小红书 你的文章也可以这样分发 墨衍会员 ¥399起/年,写一次发全网 我也要

接手过 MySQL 慢日志排查的同学,多半都经历过这种场景:打开慢日志文件,几万条记录堆在眼前,SELECT、UPDATE、DELETE 混在一起,有的执行了 0.2 秒,有的跑了几十秒;有的 SQL 一天出现几千次,有的只出现一次但一次就卡死半个库。你盯着日志翻半天,眼睛都花了,还是分不清到底该先改哪条。这事的本质是慢日志只回答了“哪些语句慢”,但没有回答“哪些语句慢得最有价值”。而 pt-query-digest 这个工具,恰好就是用来做二次筛选的。

这篇文章我会围绕“三步定位最值得修的 Top SQL”这条主线,完整演示一遍从慢日志采集、pt-query-digest 分析,到最终锁定优化目标的过程。顺手还会分享一个我平时常用的 Python 可视化看板思路,解决“分析报告出了,但值班同事看不懂”这个配套问题。适合被慢日志淹没的 DBA、后端开发,以及所有正在为数据库性能挠头的人。

1. 慢日志为什么值得看,以及 Top SQL 的“可修性”判断

1.1 慢日志不是越多越好,而是越“可修”越好

先把一个误区说透:慢日志多,不等于数据库有问题,更不等于每条慢 SQL 都值得优化。

我见过不少团队,一看到慢日志数量上涨就紧张,然后开始一条条手工分析。实际上,慢日志的价值密度很低,里面通常混着三类东西:

  • 偶发性慢查询:比如某个凌晨的批量任务,跑了一次 30 秒的大更新,之后再也不出现,修它的意义有限。
  • 高频小慢查询:单次执行 0.3 秒,听起来不严重,但一天跑 20 万次,累计消耗的时间非常可观。
  • 低频大慢查询:一天只跑几次,但每次 20 秒以上,对用户体验和连接池占用都是实打实的冲击。

真正值得优化的,是后两类中的一部分。判断标准不是“慢”,而是 “慢得稳定、慢得有规律、慢在可改变的路径上”。比如一条 SQL 因为缺少索引导致的全表扫描,加个索引就能从 5 秒降到 50 毫秒,这是高性价比优化;而一条 SQL 慢是因为它要跨 8 张表做聚合计算,即使优化也有天花板,那就要评估是否改业务逻辑。

1.2 三步定位的总体思路

我在实际处理中,会把整个流程压缩成三步:

  1. 用 pt-query-digest 对慢日志做聚合分析,把几万条原始记录压缩成一张“Top SQL 排名表”。
  2. 从排名表里筛选出“高价值慢 SQL”:优先看执行次数多、总耗时高、平均耗时异常的语句。
  3. 针对筛选出的 SQL,回查执行计划、表结构、索引情况,确认优化手段后动手。

这篇文章的标题写成“三步定位最值得修的 Top SQL”,核心就是这个逻辑。用 pt-query-digest 不是为了省掉人工判断,而是把人工判断的范围从“几万条记录”缩小到“十几条候选语句”。这个工具不会替你做决策,但它能帮你把决策的输入整理干净。

2. 第一步:开启慢日志与采集样本

2.1 慢日志参数配置与注意点

pt-query-digest 分析的对象是慢日志文件,所以第一步是确保 MySQL 把该记的 SQL 都记下来。参数层面,通常关注这几个:

INI
slow_query_log = ON
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
log_queries_not_using_indexes = ON
min_examined_row_limit = 100

这里有几个容易被忽略的细节:

  • long_query_time 单位是秒,支持小数。生产环境如果追求精细,可以设成 0.5 甚至 0.2,但不建议一开始就设太严,否则慢日志文件增长会非常快,pt-query-digest 解析起来也费劲。我习惯先用 1 秒跑一周,后续根据业务情况再收紧。
  • log_queries_not_using_indexes 建议打开。它的作用是记录那些没走索引的查询,哪怕执行时间没超过阈值。这类 SQL 往往是最值得优化的,因为加索引的效果立竿见影。
  • min_examined_row_limit 用于过滤扫描行数过少的语句,防止日志被大量简单查询刷屏。这个值要根据业务特点设,不是越大越好。

注意:修改这些参数后,需要确认 MySQL 版本对动态参数的支持情况。大多数参数在 5.7、8.0 里都可以用 SET GLOBAL 在线调整,但 slow_query_log_file 的修改在某些版本下不会自动生效,稳妥起见还是改配置文件后重启实例,或者用官方推荐的方式切换日志文件。

2.2 采集周期与样本量控制

慢日志的采集策略,决定了分析结果的代表性。我见过有人只收集了半小时的慢日志就去分析,结果 Top SQL 全是某个定时任务的语句,完全看不出常态问题。

建议的采集方式有三种:

  • 短期快照:遇到线上故障时,取故障前后 30 分钟到 1 小时的慢日志,重点看“此刻谁在拖垮数据库”。
  • 周期归档:每天凌晨用 logrotate 或 crontab 切割一次慢日志,保留最近 7 天,按天分析,观察趋势。
  • 长周期合并:把一周的慢日志合并后再交给 pt-query-digest,得到一个更加平滑的排名。适合做周度巡检。

样本量不建议太大。我个人的经验是:单次分析控制在 5 万到 20 万条慢查询记录之间比较舒服。太少了看不出规律,太多了 pt-query-digest 虽然能处理,但输出报告里各种百分比会变得很钝,反而掩盖了少数极端 SQL 的问题。

实际采集时还需要注意一个细节:MySQL 的慢日志里记录的时间是语句执行完成的时间,不是开始时间。如果一条 SQL 跑了 300 秒,它会被记到“结束那一刻”的日志段里。分析时如果你按小时切分日志文件,要注意这种边界偏移。

3. 第二步:用 pt-query-digest 生成报告,先看“摘要页”

3.1 安装与基础命令

pt-query-digest 是 Percona Toolkit 的核心工具之一。安装方式不复杂,在 Ubuntu/Debian 上可以这样:

BASH
apt-get install percona-toolkit

CentOS/RHEL 上则是:

BASH
yum install percona-toolkit

也可以直接下载 Percona 官方的 tarball 解压使用,里面的 pt-query-digest 是 Perl 脚本,依赖几个 Perl 模块。离线环境安装时,最稳妥的方式是找一台能联网的同版本机器,把依赖装好之后把整个工具链打包带过去。

基础分析命令非常简单:

BASH
pt-query-digest /var/log/mysql/mysql-slow.log > digest_report.txt

输出会写入 digest_report.txt。打开这个文件,你会看到一份结构非常清晰的报告。第一次接触这工具的人,容易对着几百行报告发懵。别急,我带你拆一下。

3.2 摘要报告怎么看:不要被 Profile 带偏

一份典型的 pt-query-digest 报告,长这样(关键部分):

TEXT
# Profile
# Rank Query ID Response time Calls R/Call V/M Item
# ---- ----------------------------------- --------------- ----- ------ ----- ----
# 1 0x1234567890ABCDEF1234567890ABCDEF 1256.3457 52.3% 10234 0.1228 0.01 SELECT user_order_list
# 2 0x0FEDCBA9876543210FEDCBA9876543210 834.0021 34.7% 32 26.0626 0.99 UPDATE inventory_stock
# 3 0x112233445566778899AABBCCDDEEFF00 312.5540 13.0% 5230 0.0598 0.01 SELECT product_detail

很多人一上来就盯着 Response time 最高的那条,觉得“最慢的必须最先修”。这个方向不一定错,但不够全面。

我自己看这份 Profile 时会按顺序做三件事:

  1. 看 Calls(执行次数)。Calls 特别高的,通常是业务高频路径,修一条能解放一大片。
  2. 看 R/Call(平均单次响应时间)。如果 R/Call 很高,但 Calls 很少,说明这是“重量级低频语句”,也要留意。
  3. 看 Response time 占比。这个数据反映的是总消耗,能帮你判断这条 SQL 对整体性能的贡献度。

注意 Profile 表里的 Query ID 是 pt-query-digest 根据 SQL 指纹生成的哈希值。它会忽略字面量、空格、大小写差异,把结构相同的 SQL 归为一类。这就是为什么你看到 1 万条 SELECT ... WHERE user_id = ? 会被合并成一条记录,而不是散落成 1 万行。

3.3 用 --limit 精确圈定 Top SQL

默认情况下,pt-query-digest 输出的 Profile 会列出很多条。我习惯加参数限制输出范围,减少干扰:

BASH
pt-query-digest --limit=20% --outdir=/tmp/digest /var/log/mysql/mysql-slow.log

--limit 的取值有两种:

  • 百分比:--limit=20% 表示输出累计 Response time 占比前 20% 的语句。
  • 行数:--limit=10 表示只输出排名前 10 条。

在实际操作中,我一般用 --limit=10 到 --limit=30 之间。Top 10 适合快速定位最严重的几条,Top 30 适合做周度分析。如果你觉得默认输出顺序不直观,可以用 --order-by 调整排序维度:

BASH
pt-query-digest --order-by="calls:sum" /var/log/mysql/mysql-slow.log

calls:sum 表示按总执行次数排序。这个排序方式特别适合“高频小慢查询”场景。比如某个接口每次请求都会触发一条 0.5 秒的 SQL,单看不严重,但每天百万次请求就是巨大的资源浪费。按次数排序能让你第一眼就看到它。

4. 第三步:锁定最值得修的三类 Top SQL

4.1 两类误伤:短查询高频 vs 全表扫描重语句

用 pt-query-digest 拿到 Top SQL 后,还不能直接开改。我遇到的最常见误区有两个。

第一个误区是“只看次数”。执行次数最多不代表最值得修。有些查询虽然被调用了很多次,但每次只扫描几行,平均响应时间在几十毫秒以内,只是因为调用频率实在太高才进了慢日志排名。这种 SQL 的优化空间很小,除非你能从业务层面减少调用次数,否则在数据库层面很难榨出太多收益。

第二个误区是“只看平均耗时”。有些 SQL 平均耗时高,是因为某几次执行特别慢,拉高了平均值。这时候要看 pt-query-digest 输出里的分位数信息。工具会给出 95%、99% 等百分位的耗时数据。如果 p95 很低,只有 max 很高,那这可能是偶发问题,比如锁等待、磁盘 IO 抖动,而不是 SQL 本身的结构问题。优化前一定要把这两类分开。

4.2 用 pt-query-digest 输出适合优化的字段

pt-query-digest 的一个强大之处,在于它不只输出排名,还会为每条 Top SQL 输出一段详细统计。在报告里找到对应 Query ID 后,你会看到这样的内容:

TEXT
# Tables
# SHOW TABLE STATUS LIKE 'user_order_list'\G
# SHOW CREATE TABLE user_order_list\G
# EXPLAIN /*!50100 PARTITIONS*/
SELECT user_id, order_id, amount FROM user_order_list
WHERE user_id = 1 ORDER BY create_time DESC LIMIT 20\G

这正是我最依赖的部分。它已经把优化需要的三样东西备齐了:

  • 涉及的表名。
  • 建表语句提示。
  • 可以直接执行的 EXPLAIN 语句。

拿到这些信息后,我通常会快速做一轮分析:

SQL
EXPLAIN SELECT user_id, order_id, amount FROM user_order_list
WHERE user_id = 1 ORDER BY create_time DESC LIMIT 20;

重点看 type 字段是否为 ALL(全表扫描),key 字段是否为空,rows 字段估算的扫描行数是否远大于实际返回行数。如果条件列上没有索引,而 rows 又是几十万甚至上百万,那么优化手段基本可以确定为“加联合索引”。如果 rows 不大,但 Extra 里出现 Using filesort,那就要考虑是否调整索引顺序来消除排序。

这类 SQL 修复后收益非常直观:从全表扫描变成索引范围扫描,执行时间通常能从秒级降到毫秒级。这也是为什么我一直强调,Top SQL 不是看排名,而是看“可修性”。一条扫描 100 万行但只返回 20 行的 SQL,比一条扫描 2000 行返回 2000 行的 SQL 更值得修,因为前者有明确的索引优化路径。

4.3 Python 可视化看板补充:慢日志统计与趋势联动

光有 pt-query-digest 的文本报告还不行。说实话,文本报告在每周巡检时够用,但你让业务研发同学直接看 digest 报告,他们大概率会退缩。而慢日志分析的价值,最终要落到“研发愿意改 SQL”上,这里就需要一个可视化看板了。

我常用一个轻量的 Python 方案:用 pandas 解析 pt-query-digest 的输出,或者直接解析 MySQL 慢日志文件,然后把统计结果存到 SQLite/MySQL,再用 Flask/FastAPI 提供一个简单的 Web 页面。

核心逻辑不复杂:

  1. 用 pandas 读取慢日志或 digest 报告,提取每条 SQL 指纹、执行次数、总耗时、平均耗时、最大耗时、涉及表名。
  2. 按小时/天聚合,绘制趋势图,观察慢查询数量的波峰波谷。
  3. 生成 Top SQL 表格,支持按“执行次数”“总耗时”“平均耗时”排序。
  4. 点击某条 SQL,自动展示对应的 EXPLAIN 提示和最近几次出现的时间点。

严格来说,这个看板不一定要跟 pt-query-digest 深度集成。你可以用 pt-query-digest 做离线深层分析,用 Python 看板做日常趋势监控。两者互补,各有侧重。

5. 实战:把 pt-query-digest 结果做成一个可筛选的 Top SQL 清单

5.1 一次完整命令串联

纸上谈兵结束,下面是一套我常用的完整操作序列。假设现在要分析最近 7 天的慢日志,文件按天切割,存放在 /var/log/mysql/slow/ 目录下。

第一步,合并日志:

BASH
cat /var/log/mysql/slow/mysql-slow-*.log > /tmp/all_slow.log

注意:如果文件很多,直接用 cat 可能会产生一个超大文件。更稳妥的方式是用 find 配合 xargs:

BASH
find /var/log/mysql/slow/ -name "mysql-slow-*.log" -mtime -7 -print0 | xargs -0 cat > /tmp/all_slow.log

第二步,运行 pt-query-digest:

BASH
pt-query-digest --limit=30 --outdir=/tmp/digest /tmp/all_slow.log

如果日志文件很大,比如超过 1GB,建议加一个过滤条件,只分析执行时间超过 2 秒的语句:

BASH
pt-query-digest --filter '$event->{time} > 2' --limit=30 /tmp/all_slow.log

--filter 参数后面跟的是 Perl 表达式,$event->{time} 是单条慢查询的执行时间,单位秒。这个参数非常实用,特别是在面对超大规模日志时,可以先粗筛再细看。

第三步,查看摘要:

BASH
head -150 /tmp/digest/analyzed-*.log

5.2 解析结果落库与看板联动

跑完 pt-query-digest 后,你得到的是文本报告。如果要做可视化,可以先把关键字段解析出来,落库。

我常用的做法是写一个简短的 Python 脚本,通过正则从 digest 报告里提取 Profile 表格,再用 pandas 转成 DataFrame。示例解析逻辑是这样:

PYTHON
import re
import pandas as pd
 
with open('/tmp/digest/analyzed-slow.log', 'r', encoding='utf-8') as f:
content = f.read()
 
# 提取 Profile 部分的每一行
profile_lines = []
in_profile = False
for line in content.splitlines():
if line.startswith('# Profile'):
in_profile = True
continue
if in_profile:
if line.startswith('#') and len(line) > 2:
profile_lines.append(line)
else:
break
 
# 解析字段
records = []
for line in profile_lines:
m = re.match(r'#\s+\d+\s+(\S+)\s+([\d.]+)\s+([\d.]+)%\s+(\d+)\s+([\d.]+)\s+([\d.]+)\s+(.*)', line)
if m:
records.append({
'query_id': m.group(1),
'response_time': float(m.group(2)),
'pct': float(m.group(3)),
'calls': int(m.group(4)),
'r_call': float(m.group(5)),
'v_m': m.group(6),
'item': m.group(7).strip()
})
 
df = pd.DataFrame(records)
print(df.head(10))

这个脚本在数据量不大时够用。如果慢日志条目特别多,或者你想要更细的维度,就不建议解析 pt-query-digest 的报告了,直接用 Python 的慢日志解析库(比如 mysqld-logs 或自定义正则)去读原始慢日志文件,按 SQL 指纹聚合。效果类似,但灵活度更高。

我自己的习惯是:pt-query-digest 管深度分析,Python 脚本管趋势和看板。看板上的 Top SQL 排名,主要为了值班同学快速发现问题;真到动手优化时,还是回 pt-query-digest 报告里挖细节。

6. 常见问题与排查技巧实录

6.1 慢日志文件过大、解析慢怎么办

慢日志动辄几个 GB 的话,pt-query-digest 解析会变得很慢,甚至卡死。我的处理顺序是这样:

  1. 按时间范围切片:sed -n '/2025-05-01 00:00:00/,/2025-05-02 00:00:00/p' mysql-slow.log > slice.log。
  2. 用 --filter 粗筛,只保留执行时间超过阈值的语句。
  3. 用 --no-report 关闭不必要的报告输出,只保留 Profile 摘要。

另外一个小技巧:pt-query-digest 支持直接读取 gzip 压缩的日志,所以归档时建议顺手压缩一下,既能省磁盘,也能减少 IO 压力。

6.2 pt-query-digest 打不出来或字符集问题

如果运行 pt-query-digest 时直接报错,或者输出报告里的 SQL 中文变成乱码,通常是 Perl 环境和字符集的问题。

先检查依赖:

BASH
perl -MDBI -e 'print "ok\n"'

如果报错找不到 DBI 模块,就安装 libdbd-mysql-perl。字符集问题则可以在命令里指定:

BASH
pt-query-digest --charset=utf8mb4 /var/log/mysql/mysql-slow.log

MySQL 8.0 默认字符集是 utf8mb4,而 pt-query-digest 默认可能按 utf8 解析日志里的语句,表名或注释中有 emoji 等字符时,就会导致解析异常。这个参数虽然简单,但真能救命。

6.3 明明很慢却不进慢日志

这个问题看起来低级,实际非常常见。排查顺序如下:

  1. 确认 slow_query_log 是否真的打开了:SHOW VARIABLES LIKE 'slow_query_log';
  2. 确认 long_query_time 的单位和大小。注意,修改 long_query_time 后,新连接才会生效,已有的连接可能仍按旧阈值记录。
  3. 确认 log_queries_not_using_indexes 有没有因为 min_examined_row_limit 的过滤而失去效果。比如你设了 min_examined_row_limit=1000,那么扫描 500 行但没走索引的查询就不会被记录。
  4. 确认慢日志文件路径是否有写入权限。有时候 MySQL 进程对目录没写权限,日志静默丢失。

6.4 看板数据与 pt-query-digest 对不上

Python 看板直接解析慢日志,和 pt-query-digest 统计的数字有时候会不一致。主要原因是 SQL 指纹算法不同。

pt-query-digest 会把 IN (1, 2, 3) 和 IN (4, 5, 6) 归为同一类,而简单的正则替换可能做不到这么智能。如果你发现两者 Top SQL 排名差异很大,先别急着怀疑解析代码,先核对一下“合并口径”。

我踩过两次坑之后,现在的做法是:看板只负责“发现异常”,具体的 SQL 指纹聚合和排名交给 pt-query-digest。看板里直接展示 digest 的 Profile 结果,不自己另起炉灶做聚合,避免两套标准打架。

写在最后

用 pt-query-digest 分析慢日志这件事,做得多了会发现,真正的瓶颈往往不在工具,而在“取舍”。几万条慢查询里,能修的、值得修的、修了有效果的,可能就那么十几条。工具帮你把范围缩小了,剩下的判断还是要靠对业务的理解和对执行计划的敏感度。

我个人这几年最大的体会是:慢日志报表做得再漂亮,不如趁热把 Top 3 的 SQL 修掉。每修一条,慢日志总量就会肉眼可见地下降,那种爽感是任何看板都替代不了的。希望这篇内容能帮你把第一刀切在正确的地方。

相关推荐

基于Python+Open CV的手势识别算法设计

伴随着人工智能时代的到来,人机交互的领域也逐渐成为研究的一大方向;其中,手势识别是人机交互领域的一项关键技术,自此手势识别也迎来了一波高潮,近几年无论是在消费领域、电子领域、数码领域、家电领域甚至汽车领域上,都能或多或少的见到手势识别的身影。 手势识别在设计智能高效的人机界面方面具有至关重要的作用, 目前手势识别已应用到手语识别、智能监控、到虚拟现实等各个领域,手势识别的原理都是利用各种传感器(例如红外、摄像头等)对手部的形态进行捕捉并进行建模,形成一个模型信息的序列帧,而后将这些信息序列转换为机器能够识别的相对应指令(例如打开、切换菜单、移动等)来完成控制。 采用Python的集成开发环境Pycharm进行本次课程设计,在Pycharm中进行需要库(模块)的下载,调取电脑摄像头,按帧读取摄像头采集到的头像,形态学处理,图像旋转(由于摄像头采集到的图像是镜像,需要用cv2.flip将摄像头采集到的图像进行处理),选取图片中固定位置作为手势输入,用红线画出手势识别框,基于hsv的肤色检测,进行高斯滤波,找出轮廓,求出图像中手势的凹凸点,手指间角度求取。

努力学习前端的小陈

ROS-CV-ImageConverter:将ROS图像转换为OpenCV(cv :: Mat)类型的图像

ROS-CV图像转换器 依存关系 cv_bridge(ROS软件包)

Willis Wang

计算机视觉图像处理-CV2与图像插值算法

用最近邻插值与双线性插值原理完成图像缩放 最近邻插值,是指将目标图像中的点,对应到源图像中后,找到最相邻的整数点,作为插值后的输。缺点:使用最近邻插值算法处理图像放大时,在图象中可能出现明显的块状效应 双线性插值,在两个方向上做三次线性插值。使用双线性插值算法处理图像放大时,在图象中可能出现马赛克 代码 import cv2 img = cv2.imread(’./xiabang.jpg’, cv2.IMREAD_UNCHANGED) #缩放比例 scale_percent = 0.3 width = int(img.shape[1] * scale_percent) height = int

weixin_38697274

基于水平集CV模型的图像分割(Matlab代码)

基于水平集CV模型的图像分割(Matlab代码),全套代码

小宇QAQ

导向滤波(guide_filter)的python实现

导向滤波(guide_filter)的python实现,可以结合暗通道;理论实现比较好的图像去噪效果。

zachzhan_g
返回顶部
清浅池塘
博客等级 码龄9年 43粉丝 8,119原创