MySQL 慢查询日志分析实战:pt-query-digest 报告解读、EXPLAIN 定位缺失索引与四种典型慢查询优化

网站变慢的元凶,往往藏在数据库里

很多站长的优化路径是这样的:先加内存、再换 SSD、然后上缓存插件、最后换成 LNMP 架构,速度还是不满意。折腾一圈之后打开 top 一看,CPU 高得吓人的其实是 mysqld,而 Nginx 和 PHP-FPM 反倒很闲。这时候问题就很清楚了——瓶颈在数据库的慢查询上。

对个人站来说,数据库调优的投入产出比极高:你不需要换服务器,不需要重写代码,只要找出那几条拖后腿的 SQL,加个索引或者改个写法,页面加载时间可能直接从 3 秒降到 300 毫秒。而找出慢查询的工具链非常成熟:MySQL 自带的慢查询日志(slow query log)加上 Percona 的 pt-query-digest,可以说是每个站长都该掌握的基础技能。

本文从零开始,讲清楚怎么开启慢查询日志、怎么用 pt-query-digest 分析、怎么读懂它输出的报告、以及几种最常见的慢查询模式和对应的优化方法。

第一步:开启慢查询日志

慢查询日志默认是关闭的。开启有两种方式:临时用 SQL 命令(重启失效,适合调试),或者写进配置文件(永久生效,推荐)。

-- 临时开启(当前会话/全局生效,重启失效)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;          -- 超过 1 秒记入日志
SET GLOBAL log_queries_not_using_indexes = 'ON';  -- 没走索引的也记录

-- 查看当前状态
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

永久生效则改 /etc/mysql/mysql.conf.d/mysqld.cnf 或 /etc/my.cnf:

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

参数说明:

  • long_query_time:单位秒,可带小数(比如 0.5)。个人站建议先设 1 秒,抓大问题;优化到一定程度后再降到 0.5 秒或 0.2 秒。设成 0 会记录所有查询,日志爆炸,不要用。
  • log_queries_not_using_indexes:记录没走索引的查询。注意它对小表也会记录,日志可能非常吵,高峰期建议关掉,或者配合 min_examined_row_limit 使用。
  • min_examined_row_limit:扫描行数低于这个值的查询不记录,能过滤掉"没走索引但表很小"的噪音。

配置改完要重启 MySQL 或用 SET GLOBAL 动态生效。日志文件记得配 logrotate,否则长期运行可能涨到几个 G。Debian/Ubuntu 下装 mysql-server 时会自动带 /etc/logrotate.d/mysql-server,检查一下路径是否对得上。

第二步:看懂慢查询日志的原始格式

分析之前先瞄一眼原始日志,心里有个数:

# Time: 2026-10-02T09:15:01.123456Z
# User@Host: webuser[webuser] @ localhost []  Id:   8821
# Query_time: 3.204517  Lock_time: 0.000112 Rows_sent: 20  Rows_examined: 1284390
SET timestamp=1759396501;
SELECT id, title, content FROM wp_posts
  WHERE post_type='post' AND post_status='publish'
  ORDER BY post_date DESC LIMIT 20;

关键字段:

  • Query_time:这条 SQL 执行耗时,3.2 秒。
  • Lock_time:等锁的时间。
  • Rows_sent:返回了多少行。
  • Rows_examined:扫描了多少行。这个值是重点——扫描了 128 万行只返回 20 行,说明索引严重缺失,典型的全表扫描。

一条查询的 Rows_examined / Rows_sent 比值越大,越说明需要索引优化。理想情况是相近,比如扫描 25 行返回 20 行。

第三步:安装 pt-query-digest

原始日志能看,但日志一多就抓瞎。Percona Toolkit 里的 pt-query-digest 能把日志聚合成一份清晰的报告,按总耗时排序,一眼看出哪条 SQL 最该优化。

# Debian/Ubuntu
apt-get install percona-toolkit

# CentOS/RHEL
yum install percona-toolkit
# 或者直接下二进制

如果软件源里没有,也可以从 Percona 官网下载 .deb / .rpm 包,或者用 cpana 装。pt-query-digest 本身是 Perl 脚本,依赖 perl-DBI,装完 pt-query-digest --version 能输出版本号就算成功。

第四步:生成分析报告

最常用的一条命令:

pt-query-digest /var/log/mysql/slow.log > /tmp/slow_report.txt
less /tmp/slow_report.txt

也可以直接分析正在跑的、或者指定时间段的日志:

# 只看最近 1 小时的日志(先用 head 或 tail 定位)
pt-query-digest --since '1h' /var/log/mysql/slow.log

# 指定某个查询的详细报告
pt-query-digest --filter '$event->{arg} =~ m/^SELECT/i' /var/log/mysql/slow.log

# 从 tcpdump 抓的包里分析(进阶,可分析未开启慢日志的库)

报告分成三大块:总体概览、Profile、单条查询详情。逐一看。

第五步:读懂报告第一部分——总体概览

# Overall: 1.23k total, 45 unique, 0.06 QPS, 0.71x concurrency
# Time range: 2026-10-02 08:00:00 to 2026-10-02 14:00:00
# Attribute          total     min     max     avg     95%  stddev  median
# ============     ======= ======= ======= ======= ======= ======= =======
# Exec time        1245s     1s      28s     1s      2s      ...
# Lock time          3s       0       120ms   2ms
# Rows sent         24.6k       0     2.3k   20
# Rows examine      89.2M       0     1.2M   72.4k
# Query size       621k      120    8.2k    505

这里最值得关注的是 Rows examine 总量 和 Exec time 总量。如果 Rows examine 是个巨大的数字(几千万上亿),说明存在大量全表扫描,索引是主要矛盾。

如果日志里有 45 个 unique query,你不用一个一个看——报告会按"总耗时"排序,往往前 3 条就占了 60%~80% 的时间。这就是二八法则在数据库调优里的体现。

第六步:读懂报告第二部分——Profile(谁最耗时)

# Profile
# Rank Query ID           Response time   Calls R/Call Items
# ==== ================== =============== ===== ====== =====
#    1 0x8A9C1D2E3F4A5B6C  812.4s  65.2%   120  6.77s SELECT wp_posts
#    2 0x1B2C3D4E5F6A7B8C  340.1s  27.3%    80  4.25s SELECT wp_options
#    3 0x9F8E7D6C5B4A3F2E   60.2s   4.8%   200  0.30s UPDATE wp_postmeta

排名第一的这条 SELECT wp_posts,120 次调用、总耗时 812 秒、平均每次 6.77 秒,占了总耗时的 65%。优化它,收益立竿见影。

典型的 WordPress 站,慢查询重灾区往往是:wp_options(autoload 选项太多)、wp_postmeta(meta 查询)、wp_posts 的复杂查询、以及 ORDER BY ... LIMIT 没有索引的情况。

第七步:读懂报告第三部分——单条查询详情

# Query 1: 0.14 QPS, 0.71x concurrency, ID 0x8A9C1D2E3F4A5B6C at byte 8821
# This item is included in the report because it matches --limit.
# Scores: V/M = 0.44
# Time range: ...
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count         10     120
# Exec time     65    812s      2s      28s      7s      9s      4s      6s
# Lock time      0     40ms       0     12ms   333us   280us     ...
# Rows sent      0      2.4k       0      40     20      20       0      20
# Rows examine  91    81.2M    ...     1.2M   692k    ...
# Query size     1     14.4k     120     120     120
# String:
# Databases    zz1984
# Hosts        localhost
# Users        webuser
# Query_time distribution
#   1us
#   ...
#  100ms  #
#    1s   ################################################################
#   10s+  ####
# Tables
#    SHOW TABLE STATUS LIKE 'wp_posts'\G
#    SHOW CREATE TABLE `wp_posts`\G
# EXPLAIN /*!50100 PARTITIONS*/
SELECT id, title, content FROM wp_posts
  WHERE post_type='post' AND post_status='publish'
  ORDER BY post_date DESC LIMIT 20\G

这条最关键:

  • Rows examine 81.2M / Rows sent 2.4k:扫描量与返回量差了三万多倍,铁定全表扫描。表应该是几十万行到了百万级。
  • Query_time distribution:能看到耗时分布,1s 到 10s 占大头,说明是稳定的慢,不是偶发。
  • 报告最后会贴出完整的 SQL,并且提示你可以用 EXPLAIN 分析。Percona Toolkit 很贴心,直接给出了 SHOW CREATE TABLE 和 EXPLAIN 的模板。

第八步:用 EXPLAIN 找出缺的索引

拿到慢查询的原始 SQL,直接丢给 EXPLAIN:

EXPLAIN SELECT id, title, content FROM wp_posts
  WHERE post_type='post' AND post_status='publish'
  ORDER BY post_date DESC LIMIT 20;

输出里重点看这几列:

列名要看什么
typeALL 表示全表扫描,最差;index 次差;range 尚可;ref/eq_ref/const 最好
key实际用到的索引,NULL 表示没用索引
rows预估扫描行数,这个数要尽量小
Extra出现 "Using filesort"(额外排序)或 "Using temporary"(临时表)要警惕

针对上面的查询,合适的索引是:

ALTER TABLE wp_posts
  ADD INDEX idx_type_status_date (post_type, post_status, post_date);

为什么是这个顺序?因为 MySQL 索引的最左前缀原则:WHERE 里的等值条件 post_type、post_status 放前面,ORDER BY post_date 放最后,这样既能快速过滤又能直接利用索引有序性避免 filesort。加了索引后再 EXPLAIN,type 会变成 range 或 ref,rows 大幅下降,Extra 里的 filesort 也会消失。

几种最常见的慢查询模式与对策

模式一:SELECT * 加上深分页。 LIMIT 100000, 20 这种写法 MySQL 会先扫描前 10 万行再丢弃,越翻越慢。改成延迟关联或游标分页:

-- 慢
SELECT * FROM wp_posts ORDER BY id LIMIT 100000, 20;

-- 快:先用索引拿主键,再回表
SELECT p.* FROM wp_posts p
  JOIN (SELECT id FROM wp_posts ORDER BY id LIMIT 100000, 20) t ON p.id = t.id;

模式二:函数作用在索引列上。 WHERE DATE(created_at) = '2026-10-02' 会让索引失效,应改成范围查询:

WHERE created_at >= '2026-10-02 00:00:00'
  AND created_at < '2026-10-03 00:00:00'

模式三:隐式类型转换。 字段是 varchar 却传了数字,或者反过来,索引都会失效。WHERE user_id = 123(字段是字符串)会全表扫。检查字段类型和传参类型是否一致。

模式四:OR 条件。 WHERE a=1 OR b=2 往往无法用索引,可以拆成两个查询 UNION ALL。

第九步:把优化变成常态

调优不是一次性的。建议建立这样的循环:

  1. 开启慢查询日志,long_query_time 从 1 秒起步。
  2. 每周跑一次 pt-query-digest,看有没有新的慢查询冒出来。
  3. 对 Top 3 的慢查询做 EXPLAIN,该加索引加索引,该改写法改写法。
  4. 定期用 pt-duplicate-key-checker 清理冗余索引,用 pt-index-usage 找出没被用到的索引。

另外提一句:索引不是越多越好。每个索引都会拖慢写入(INSERT/UPDATE),也会占用磁盘。个人站的表通常不大,几个精准的复合索引就够,不要盲目照搬大厂方案。

写在最后

数据库调优听起来高大上,但落到个人站上其实就是"找到那条最慢的 SQL,给它加个对的索引"。慢查询日志负责找,pt-query-digest 负责排序,EXPLAIN 负责诊断,ALTER TABLE 负责解决——这套组合拳的成本几乎为零,收益却立竿见影。与其不停加配置升级服务器,不如先花一个小时把慢查询理一遍,你会发现很多性能问题根本不缺硬件,缺的只是一个索引。

Last modification:October 2nd, 2026 at 09:24 pm

Leave a Comment