用pt-query-digest三步定位慢SQL:从慢日志到索引优化实战
早上到公司打开监控,慢日志文件一晚上涨了2个G。切到文件里看了一眼,几千条慢查询齐刷刷堆在那里,每一条都标着 Query_time,但你根本分不清哪一条是昨晚业务高峰拖垮系统的元凶,哪一条只是凌晨跑批偶尔抽风。绝大多数人的第一反应是:把文件拖进编辑器,按执行时间排个序,挑出那条跑了 8 秒的 SQL 去 EXPLAIN。这是面对慢日志最直接的动作,也是效率最低的一种。
如果你手头管理的 MySQL 实例慢日志多到已经看不过来,我强烈建议你别再手工翻了。这个场景下真正该做的是用工具把慢日志做一次“结构化拆解”,让每条 SQL 按指纹聚合成报表,直接告诉你哪几类 SQL 总耗时最高、执行次数最多、平均耗时最长。能做到这件事的经典工具就是 Percona Toolkit 里的 pt-query-digest。它不会直接告诉你“优化这条”,但按我下面要说的三步定位法,你能在十分钟内从几千条慢 SQL 里捞出最值得修的 Top SQL,并且用数据说服自己:先改这一条,收益最大。
这篇文章我会先把慢日志的常见误区和 pt-query-digest 的部署讲透,再给你一套完整的三步定位方法,配合一个我本地复盘的订单查询优化案例,最后把我在实战中踩过的坑一并列出来。不管你是 DBA、后端开发还是负责数据库稳定的 SRE,这套流程拿过去就能用。
1. 慢日志泛滥时,大多数人第一步就做错了
先说一个比较扎心的现实:慢日志不是设计来给你“人肉查看”的。它本质上是 MySQL 在查询执行结束时,把超过阈值的语句追加到一个文件里,这个文件没有任何索引、没有任何聚合,每秒可能写入几十条。当业务流量上来,慢日志涨到几百 MB 甚至几个 G,再用打开文件的方式去分析就是给自己找罪受。
1.1 慢日志爆掉的两个信号:磁盘告警和监控面板飘红
我接手过的项目里,慢日志“爆掉”最常见的表象是两个:一是数据库服务器磁盘使用率告警,一查发现 /var/lib/mysql 目录下 slow.log 占了几十 G;二是监控面板上的慢查询数量曲线直线上升,从每分钟几百跳到每分钟几万。
这时候很多人的处理方式是把 long_query_time 调大,比如从 1 秒调到 5 秒,先让日志变小,眼不见心不烦。这个操作本身没错,但它只是堵住了“记录”这个环节,并没有解决“哪些 SQL 需要优化”这个根本问题。真正要做的恰恰相反——在日志还没有被覆盖掉之前,赶紧把它保存下来,然后用 pt-query-digest 做一次全量分析,找出那些就算阈值调到 5 秒依然频繁出现的语句。这类语句才是稳定拖垮数据库的长期问题。
慢日志膨胀本身不可怕,可怕的是你被“文件太大”这件事分散了注意力,忘了里面存着的是最真实的线上 SQL 样本,比任何测试环境的压测数据都更有说服力。
1.2 为什么“按执行时间排序”和“只盯单条慢查询”是常见的分析弯路
我自己也干过这种事:看到一条 UPDATE 语句 Query_time 8 秒,立刻复制出来,EXPLAIN 一看没有走索引,于是给字段加了索引,兴冲冲宣布优化完成。结果第二天慢日志里这条语句确实消失了,但数据库整体负载并没有明显下降,因为真正的大头是另一条每秒执行 200 次、每次耗时 0.5 秒的小查询——它的总耗时是 100 秒/秒,而那条 8 秒的语句一小时可能只出现两次。
这就是“只看单条执行时间”和“看总响应时间占比”之间的天壤之别。MySQL 的慢日志会把每条超阈值语句都记下来,但你手工翻文件时,视觉上最容易被“单次耗时最长”吸引。可对业务影响最大的,往往是“执行次数 × 单次耗时”这个乘积最大的 SQL 指纹。
另一个常见的弯路是把慢日志文件下载到本地,用 Excel 打开,然后写各种 SUMIF、AVERAGEIF 公式去做透视表。不是不能做,而是每次做都耗时半小时以上,而且没法方便地复用到下一批日志文件。pt-query-digest 之所以在这个领域很难被替代,就是因为它把所有语句做 MD5 指纹归类,自动算出每类 SQL 的执行次数、总耗时、平均耗时、95% 耗时,并按总耗时排序输出成报告。这些信息恰好就是“判断哪条值得修”最需要的数据。
1.3 认识 pt-query-digest:它能做的事比想象中多
很多人以为 pt-query-digest 只是一个把慢日志汇总成文本报告的命令行工具。没错,这是它最基础的用法,但它的能力远不止于此。它可以从 MySQL 慢日志文件、通用日志、进程列表、binlog、tcpdump 抓包等数据源解析查询,然后生成三类核心产物:
第一类是完整分析报告,包含全局 Profile、每条独立 SQL 指纹的执行统计、以及示例语句和当时的执行计划环境。第二类是经过过滤的子集报告,你可以用时间范围、SQL 关键字、执行次数、响应时间等条件圈定某一类问题,专门分析。第三类是可直接入库的统计结果,配合 --review 和 --history 参数,能把每次分析的指纹和指标写进 MySQL 表,长期积累之后,你就能对比同一批 SQL 在不同时间段的性能变化,等于给自己搭了一套轻量的 SQL 性能趋势系统。
这几点后面都会展开说。先把工具装上,你才能真正理解它输出的那些指标意味着什么。
2. 部署与基本用法:把慢日志变成一份能读的报告
pt-query-digest 是 Percona Toolkit 套件中的一个 perl 脚本,所以安装 Percona Toolkit 就能获得它。它依赖一批 perl 模块,但用官方仓库安装时依赖会自动解决。如果你不想引入 Percona 的软件源,也可以单独把脚本下载下来放到服务器上用,不过我更推荐按官方仓库方式安装,因为后续升级省心。
2.1 percona-toolkit 安装:一条命令装完
以 CentOS/RHEL 系为例,先添加 Percona 官方源,再安装:
Debian/Ubuntu 系的操作也类似:
安装完成后验证一下:
如果能看到版本信息,说明 perl 依赖都齐了。要注意的是,pt-query-digest 本质上是 perl 脚本,所以它的运行只依赖 perl 环境和相关模块,不需要安装额外的 MySQL 客户端,但如果你后面要用 --explain 功能,就需要服务器上能连接到 MySQL,并且配置好账号权限。
2.2 第一条分析命令与输出结构
安装好之后,最朴素的一条命令就是把慢日志文件喂给工具,让它输出一份报告:
慢日志文件大的时候,它可能需要跑几分钟,但不用担心,跑完之后报告会保存在 slow_report.txt 里。我这里第一次拿到一份 2G 的慢日志时也很担心 perl 脚本撑不住,实测下来大概三分多钟处理完,内存占用在几百 MB 左右,完全可接受。
打开报告,第一眼看到的是一张大表:Profile。这张表每一行代表一类 SQL 指纹,按总响应时间从高到低排列。列名依次是 Rank、Query ID、Response time、Calls、R/Call、V/M、Item。其中 Response time 列直接告诉你这个指纹的累计耗时以及它在整个慢日志总耗时中的占比,V/M 列则是方差均值比,反映这类查询耗时的稳定性。看 Profile 表,基本就能确定下一步优化目标。
2.3 几个必须养成的参数习惯:--since / --until / --limit / --filter
直接整文件分析在面对大量历史日志时不够精准,因为业务高峰和低峰的慢 SQL 特征不一样。我会在命令里加上时间过滤:
--since 和 --until 是分析慢日志的时间窗口,这两个参数不会改变原始文件,只是告诉工具只处理这个时间段内的记录。我在实际使用中通常会把时间窗口卡在业务高峰,比如早上 10 点到 12 点,这样可以专注看核心链路的问题,避免被凌晨的离线任务干扰。
--limit 控制报告里展示的 SQL 指纹数量:
这个参数非常有用,因为慢日志多的时候,Profile 表可能罗列上百条指纹,而你真正关心的只有最前面那几条。限定为 5 条后,报告会大幅缩短,重点一目了然。
--filter 则是用 perl 表达式对每条 SQL 进行条件过滤。比如我只想看 SELECT 语句:
这里的 $event->{fingerprint} 是被工具处理后生成的 SQL 指纹,也就是去掉具体参数值后的模板。通过这个写法,你可以精确过滤出某一类 SQL,再单独分析。这几个参数组合使用,基本能应付绝大多数慢日志分析场景。
3. 报告怎么看:Profile 表、Query 详情与关键指标逐项拆解
工具装好了,命令跑通了,接下来最关键的是你能不能读懂报告里的每个数字。很多人卡在这一步:报告生成了,但看不懂,最后还是回去翻原始日志。
3.1 Profile 表:Rank、Response time、Calls、V/M 背后的信号
Profile 表是报告的“执行摘要”,也是我每次分析第一个看的地方。它的结构大致如下:
| Rank | Query ID | Response time | Calls | R/Call | V/M | Item |
|---|---|---|---|---|---|---|
| 1 | 0xA1B2C3D4E5F60718 | 1256.3452 (32.4%) | 845 | 1.4868 | 0.82 | SELECT orders... |
逐列拆开看:
- Rank:按总响应时间排序后的序号,1 就是最值得关注的那类 SQL。
- Query ID:由工具根据 SQL 指纹计算出来的唯一 ID,同一类 SQL 即使参数不同,ID 也一样。
- Response time:总响应时间,括号里的是这类 SQL 在整份慢日志总耗时中的占比。这个占比比单次耗时更能说明问题。
- Calls:在分析窗口内,这类 SQL 被记录了多少次。
- R/Call:平均每次执行耗时。如果 Calls 很大,这个值哪怕只有 0.5 秒,也要高度警惕。
- V/M:方差均值比,反映耗时波动情况。V/M 较大说明这类查询的响应时间忽高忽低,可能受锁竞争、连接排队等偶发因素影响,和稳定性的关联更大。
- Item:SQL 指纹的片段,方便你定位是哪一类查询。
我拿到一张 Profile 表,通常先看 Response time 占比排名前五的指纹。如果第一条占比已经超过 30%,那意味着整个慢日志的负担集中在某几类 SQL 上,优化它们能直接让慢日志总量明显下降。
3.2 Query 详情页:从 Rows_examined 与 Rows_sent 的差距判断“读了不该读的行”
Profile 表定位到具体指纹后,往下翻就能看到这个 Query ID 的详细报告。其中有几个字段,对判断优化方向特别重要:Rows_examined(每行平均扫描行数)和 Rows_sent(每行平均返回行数)。
这两者之间的差距,基本就是数据库“白干”的量。举个例子,一条查询返回 20 行数据,但扫描了 3 万行,说明 MySQL 为了找出这 20 行,把大量无关数据也读了一遍。差距越大,越说明索引设计或 SQL 写法有问题。
这里我想强调一个容易被误解的点:Rows_examined 来自 MySQL 的统计信息,它并不是百分之百精确,但作为趋势判断完全够用。当你发现某条 SQL 的 Rows_examined 比 Rows_sent 高两个数量级时,就有充分理由去查它的执行计划,确认是不是有更优的索引路径。
3.3 表与索引信息:pt-query-digest 给出的提示只能作为线索
在 Query 详情里,有时候还能看到工具根据慢日志信息整理出的表名,以及一些关于索引的提示。要特别注意的是,pt-query-digest 的索引提示是从执行计划和执行成本里估算出来的,它不能替代你在数据库里实际执行 EXPLAIN,更不能替代你对业务数据的理解。
我见过有人看到报告里出现“recommend an index”就立刻去线上建索引,结果建完发现 MySQL 优化器根本没用新索引,因为实际过滤条件的数据分布和工具假设的完全不同。正确姿势是把报告里的提示当作切入点,然后回到数据库确认表结构、索引状态、以及执行计划里的 key 列和 rows 列,再判断要不要动手。
4. 三步定位法:从几千条慢 SQL 里捞出最值得修的那几条
现在进入正题。假设你手上有一份刚生成的 pt-query-digest 报告,慢日志总量几千条,你怎么在最短时间内确定“最值得修”的 Top SQL?我把它总结成一个三步流程,每一步都有明确的目的和产出。
4.1 第一步:圈定分析窗口,把无关流量过滤干净
不要胡子眉毛一把抓。先想清楚分析目标:是排查业务高峰期的核心链路,还是统计全天的慢查询分布?目标不同,过滤策略不同。
如果是排查高峰期问题,用 --since 和 --until 卡住高峰时段,比如上午 11 点到 12 点。这一步能过滤掉凌晨跑批、定时任务的干扰。如果是定位某类特定业务问题,用 --filter 只保留相关 SQL,比如只保留涉及某张核心表的 UPDATE。
这一步的核心逻辑是:慢日志是全局的,但你要优化的通常是某个具体时间窗口、某条具体业务链路上的问题。先把环境和噪音排除掉,后面的分析才会聚焦。我还会顺手把 --limit=10 加上,让报告只输出排名前十的指纹,避免一开始就被大量低频 SQL 分散注意力。
4.2 第二步:用“总响应时间占比”排序,找到真正拖垮业务的 Top N
时间窗口定了、噪音过滤完了,接下来看 Profile 表。注意排序依据不是 Calls,也不是 R/Call,而是 Response time 累计占比。
为什么?因为优化慢 SQL 的本质是降低总耗时。一条 SQL 执行一次需要 3 秒,但一天只跑 5 次,总共 15 秒;另一条 SQL 执行一次需要 0.2 秒,但一天跑 10 万次,总共 20000 秒。显然后者对数据库的整体压力更大,更值得优先处理。
具体操作上,我一般把 Profile 表前 5 条拿出来,计算前三名的 Response time 占比之和。如果这个和超过 60%,说明问题高度集中,修复这几条 SQL 就能让数据库负载明显改善。如果前 5 名加起来还不到 30%,说明慢日志比较分散,单点优化收益有限,这时候就需要结合 Calls 次数找高频低耗时的语句,或者检查是不是存在连接数打满、锁等待等全局问题。
4.3 第三步:逐条复现验证,用 EXPLAIN 确认根因后动手优化
Profile 表告诉你“该看哪条”,但“怎么改”还得靠你手动验证。我把这一步拆成三个动作:
第一个动作,是在报告里找到这条 SQL 的完整示例文本。pt-query-digest 会把参数值打码后的示例语句列出来,你可以据此还原出真实的 SQL 模板。
第二个动作,是拿这条 SQL 去数据库里执行一次 EXPLAIN。重点看三列:type、key、rows。type 是访问类型,如果出现 ALL,那就是全表扫描,基本可以断定索引没走对;key 显示实际使用的索引,如果是 NULL,说明没有可用的索引;rows 是优化器预估扫描行数,这个值越大,说明代价越高。
第三个动作,是根据根因选择优化方案,并预先评估影响。缺索引就补索引,但要考虑新增索引对写入性能的影响;SQL 写法的问题就改写 SQL,比如把深分页改成基于游标的查询;数据量过大导致全表扫描的,就要考虑归档或分区。
改完之后,回到业务流量复现,或者直接用优化后的语句跑一次,对比 Rows_examined 和执行耗时。我习惯把优化前后的 EXPLAIN 和执行时间记在同一张表里,方便后面复盘。
4.4 用 --review 建立 SQL 性能基线,验证优化前后效果
手动对比两三次还能接受,但如果你的库长期有慢查询,每次都要重新分析一遍再人工对比,很快就坚持不下来。这里我建议你用 pt-query-digest 的 --review 和 --history 参数,把分析结果持久化到 MySQL 里。
首次使用时先建好表结构:
这条命令会把慢日志里每个 SQL 指纹的基本信息和统计指标写入 percona_schema.query_review 表,自动去重。再配合 --history:
每次分析都会往 query_history 表插入一条历史快照。之后你就可以写简单的 SQL 去查询:某个 Query ID 在最近一周每次分析里的 Calls 和 R/Call 分别是多少,优化前后曲线一目了然。这比每次打开报告翻数字要高效得多,也是把 pt-query-digest 从“一次性工具”升级为“长期分析基础设施”的关键用法。
5. 一次本地复盘的完整案例:订单查询慢的定位全过程
流程讲完,我用一个实际的例子把整套方法串起来。这个例子来自我自己搭建的压测环境,表结构和数据都是模拟电商订单场景,慢日志里几百条记录集中指向同一个问题,非常适合用来演示定位思路。
5.1 现场现象与慢日志采样
模拟场景是这样的:订单表 orders 有 28 万行数据,表结构简化后包含 id、order_no、user_id、total_amount、status、pay_time 等字段。某天业务反馈后台订单查询很慢,页面要等三秒以上。我开启慢日志并设置 long_query_time=1,跑了半小时,拿到一份约 1200 条的 slow.log。
打开报告第一屏,Profile 表前几行很清晰:
| Rank | Response time | Calls | R/Call | Item |
|---|---|---|---|---|
| 1 | 352.4862 (68.4%) | 721 | 0.4889 | SELECT orders |
| 2 | 61.2354 (11.9%) | 38 | 1.6115 | SELECT order_items |
| 3 | 25.8867 (5.0%) | 120 | 0.2157 | UPDATE orders |
第一条指纹占总耗时的 68.4%,Calls 高达 721 次。这个信号已经很明确:优化这一条 SQL,慢日志总量能砍掉一半以上。R/Call 平均 0.49 秒,看起来不算特别夸张,但胜在次数多,累计下来对业务影响很大。
5.2 分析过程还原(命令+报告片段)
我从报告里找到第一条指纹的示例语句,还原后大致是这样:
直接在压测库上 EXPLAIN:
实际用的索引是 idx_user_id 单列索引,MySQL 先通过 user_id 定位到该用户的所有订单,大约 14500 行,然后回表读取数据,再过滤 status 和 pay_time,最后文件排序取 20 行。注意看 Using filesort,排序还额外产生了一次临时文件操作。
结合报告里的统计信息,这类查询在慢日志里 Rows_examined 均值大约是 1.4 万行,Rows_sent 均值是 20 行。扫描与返回的比值达到 700 倍,明显是在“拿索引找到人,却把该用户所有订单都翻了一遍”。
5.3 根因确认与索引优化
根因很清晰:查询条件里有 user_id、status、pay_time 三个等值和范围条件,但索引只建立在 user_id 上。MySQL 能通过它快速缩小用户范围,但无法在索引层面完成 status 和 pay_time 的过滤,只能逐行回表判断,再加上 ORDER BY id DESC 无法利用索引顺序,还得额外 filesort。
优化方案是建一个覆盖查询条件和排序需求的复合索引。考虑到过滤条件中 user_id 是等值,status 是等值,pay_time 是范围条件,我最终创建了:
同时因为业务端经常带 ORDER BY id DESC LIMIT 20,如果这张表的数据量继续增长,后续还可以评估降序索引 (user_id, status, pay_time, id DESC) 来消除 filesort。不过在 28 万行的规模下,先解决全表回表的瓶颈,收益就已经很明显了。
5.4 优化后的数据对比
索引创建完成后,我没有立刻宣布成功,而是重新跑了一遍相同的业务查询用例,并再次开启慢日志采集半小时。同样的时间窗口里,同一条 SQL 指纹的执行情况如下:
优化前:Calls 721,R/Call 0.4889 秒,总耗时 352.49 秒,慢日志占比 68.4%。 优化后:Calls 725,R/Call 0.0214 秒,总耗时 15.52 秒,慢日志占比 4.1%。
执行时间降低了大约 95%,慢日志总量从 515 秒左右降到 378 秒左右。页面响应从三秒降到两百毫秒以内。最直观的验证方式是:再查看慢日志,这条 SQL 指纹已经从 Top 1 掉到几乎可以忽略的位置。
这个例子告诉我们,单条 SQL 优化前慢不代表它“不可救药”,只是索引或写法没有匹配数据访问路径。用 pt-query-digest 把 Top 指纹找出来,再用 EXPLAIN 验证根因,优化目标非常明确。
6. 实战中反复踩的坑,写下来帮你省时间
工具和方法都齐了,但真正拿到生产环境分析时会遇到各种预料之外的情况。我把这些年用 pt-query-digest 踩过的坑整理一下,很多都是报表上看着正常,实际却会误导你的细节。
6.1 假慢 SQL:锁等待和长事务的干扰
有一种慢 SQL 特别具有迷惑性:它的执行计划极其完美,索引全走,Rows_examined 只有几十行,但 Query_time 就是居高不下。这种情况十有八九不是查询本身的问题,而是语句在等锁。
pt-query-digest 的报告里能看到 Lock_time,但这个指标只在某些日志格式下才准确,而且即便 Lock_time 显示为 0,也不代表没有锁等待。因为 InnoDB 的行锁等待时间在部分版本里会计入 Query_time 而不是 Lock_time。定位这类问题要靠 InnoDB 状态、performance_schema 里的锁等待事件,或者直接看当时的连接状态。我的习惯是:如果某条 SQL 走索引很完美但慢得出奇,先查 SHOW ENGINE INNODB STATUS 里有没有长时间的锁等待,而不是急着加索引。
6.2 EXPLAIN 结果与报告不一致时的排查思路
你可能会遇到这种情况:pt-query-digest 报告里说这条 SQL 扫描行数很多,但你复制出来在测试库 EXPLAIN,发现 rows 只有几百,怎么都对不上。
先冷静,这不一定是你复制错了。生产环境和测试库的数据分布不同,优化器统计信息不同,甚至 MySQL 版本不同都会导致执行计划差异。正确做法是到生产库的从库上执行 EXPLAIN,或者至少把表的统计信息重新 ANALYZE 一遍再验证。另外,pt-query-digest 报告里的示例语句可能不是完整原 SQL,而是指纹还原后的近似语句,字段顺序、括号嵌套都可能被标准化,导致执行计划存在细微差别。遇到这种不一致,以实际语句在目标环境上的 EXPLAIN 为准。
6.3 时区、字符集、没有慢日志权限这些边缘问题
使用 --since 和 --until 时,要注意慢日志时间记录的是 MySQL 服务器的本地时间。如果你的分析机和生产服务器时区不一致,过滤出来的时间段会偏移好几个小时,导致分析结果失真。我的做法是直接在数据库服务器上执行分析命令,或者在命令里显式指定 --use-timezone 相关参数,保证时间解释一致。
字符集问题也不少见。如果慢日志里包含一些特殊字符或者非 UTF-8 编码的 SQL,pt-query-digest 在解析时可能会报错或输出乱码。虽然大多数情况下不影响统计,但如果你发现报告里某个指纹的 Item 显示异常,可以先检查慢日志文件的编码,必要时用 iconv 转换后再喂给工具。
还有一个容易忽略的坑:分析机上没有 MySQL 客户端权限,导致 --review 写库失败。--review 需要数据库账号具备建表和 insert 权限,脚本本身不会帮你判断权限是否足够,只会直接报错。提前把账号权限配好,能省不少排查时间。
6.4 别把 pt-query-digest 当监控工具:它是离线分析器
最后想特别提一点:pt-query-digest 是离线分析工具,它读的是已经落盘的慢日志文件,不是实时监控系统。有些人一开始误以为它能像监控平台那样持续输出每分钟的 SQL 性能趋势,结果发现只能手动跑命令,就失望了。其实它的正确定位是“深度分析引擎”——负责回答“慢日志里到底哪些 SQL 最该改”,而实时告警、趋势监控应该交给专门的监控系统。
如果你需要长期跟踪 SQL 性能变化,不要把原始慢日志文件当作永久存储,一是占磁盘,二是查询不便。我现在的做法是:每天夜间定时用 pt-query-digest --review --history 把当天的慢日志分析结果入库,原始日志保留一周后清理。库里的 query_review 和 query_history 表长期留存,需要做性能趋势对比时直接查表,完全不需要重新解析旧日志。
说到底,慢日志分析这件事,工具只是帮你看清楚问题的望远镜,真正决定优化效果的是你能不能从报表里读出“哪条 SQL 最值得修”的判断力。希望这篇文章的三步定位法和案例复盘,能让你下一次面对几 GB 的慢日志时不再头皮发麻。你也可以从今天开始,把 pt-query-digest 加入你的数据库运维工具箱,先从一条最简单的分析命令跑起来,慢慢你就会发现,优化 SQL 不再是一拍脑袋的猜测,而是一条可以量化、可以验证的清晰路径。