说实话,MySQL变慢这件事,就像家里的水龙头突然出水量变小了。你第一反应肯定不是去砸墙看水管,而是先看看是不是总阀门被关小了,或者水管里堵了什么脏东西。数据库也一样,别上来就想着加服务器、换架构,先学会“体检”。
今天咱们不整那些虚头巴脑的理论,直接上干货。我会带你用两个最实用的工具——pt-query-digest 和 PMMA,把MySQL的慢查询问题扒得底裤都不剩。而且,我会把那些网上没人告诉你的“坑”都列出来,毕竟踩过的坑才算经验。
一、 先别急着查,先确认“真的慢”在哪里
很多初学者遇到性能问题,上来就打开慢查询日志,对着几千行日志发呆。这就像医生不看症状直接开刀,是大忌。
第一步:用全局视角看“哪里疼”
在执行任何深度分析之前,先问自己三个问题:
是某个SQL慢,还是所有SQL都慢?
- 如果所有SQL都慢,可能是服务器资源(CPU、IO、内存)瓶颈。
- 如果只是特定SQL慢,那是查询优化问题。
是突然变慢,还是逐渐变慢?
- 突然变慢:可能最近上线了新代码、加了表、或者有人跑了个大数据量任务。
- 逐渐变慢:可能是索引失效、数据量增长、统计信息过时。
慢在哪个环节?
- 网络延迟?(连接超时)
- 解析慢?(SQL复杂,优化器计算久)
- 执行慢?(IO多,锁等待,全表扫描)
- 返回慢?(数据量大,客户端传输慢)
实战技巧:用 SHOW PROCESSLIST 快速定位
SHOW PROCESSLIST;
别小看这个命令。你就能一眼看出:
- 有没有
Sleep连接堆积?(连接没释放,占资源) - 有没有
Locked状态?(锁冲突,典型的“卡死”) - 有没有长执行的
Query?(正在跑的大SQL)
比如,你看到一条SQL已经跑了300秒,状态是 Sending data,那基本可以确定是查询本身有问题,可能在扫描大量数据。
避坑提醒:SHOW PROCESSLIST 在连接数特别多的时候(比如上万),会拖慢数据库。这时候用 information_schema.processlist 视图更稳妥,或者直接用 pt-heartbeat 这类工具测延迟。
二、 慢查询日志:你的“黑匣子”
如果 SHOW PROCESSLIST 只能看到“正在进行时”,那慢查询日志就是“历史录像”。它能记录所有执行时间超过阈值的SQL。
1. 开启慢查询日志(如果还没开)
-- 查看当前配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启慢查询日志(临时,重启失效)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL long_query_time = 1; -- 超过1秒的SQL记录
-- 永久生效,改 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 -- 记录没用索引的SQL,这个很重要!
关键参数解释:
long_query_time:默认10秒,建议改成1秒或0.5秒。太大会漏掉很多“亚健康”SQL。log_queries_not_using_indexes:开启后,所有没走索引的SQL都会被记录,即使执行时间很短。这是发现性能隐患的金矿。
2. 慢日志的常见陷阱
陷阱1:日志文件膨胀
慢查询一旦开启,日志会越来越大。别等到磁盘满了才发现。
# 定期清理或轮转
cronjob: 0 2 * * * find /var/log/mysql -name "slow.log" -mtime +7 -delete
或者用 pt-killer 这类工具自动管理。
陷阱2:记录量太大,影响性能
如果线上SQL量巨大,全量记录慢日志会带来额外IO开销。可以:
- 只记录
log_queries_not_using_indexes - 用
pt-query-digest的--review功能,只记录重复性高的慢查询 - 开启
slow_log表(MySQL 5.6+),存到mysql.slow_log表,方便查询
三、 pt-query-digest:慢日志的“CT机”
慢日志只是原始数据,pt-query-digest 是Percona Toolkit里的神器,能把杂乱无章的日志变成结构清晰、重点突出的报告。
1. 基础用法
pt-query-digest /var/log/mysql/slow.log
运行后,你会看到一份详细的报告,包括:
- 总体统计:总执行次数、总时间、平均时间、P99时间
- TOP查询:按时间、调用次数、锁定时间等排序
- 分位数统计:了解分布情况
- 指纹分析:将相似SQL归类,避免重复分析
2. 实战案例:从一堆乱码中找到元凶
假设你的慢日志里有成千上万条记录,直接看眼睛都要瞎。用 pt-query-digest:
pt-query-digest --group-by fingerprint /var/log/mysql/slow.log
输出示例:
# Query 1: 0.53 QPS, 0.53x concurrency, ID 0x1234 at byte 12345
# This item is included because the query matches 'time' profile.
# Fields number 7
# Profile
# Rank Query ID Response time Calls R/Call V/M Item
# ==== ================ ================= ===== ====== ==== ====
# 1 0x1234ABCDEF 45.2300 (50.1%) 100 0.4523 0 SELECT users
# MISC 0xMISC 44.8000 (49.7%) 950 0.0472 0 <MISC>
# Query 1: 0.53 QPS, 0.53x concurrency, ID 0x1234 at byte 12345
# Profile
# Rank Query ID Response time Calls R/Call V/M Item
# ==== =========== ================ ====== ======== ==== ==========
# 1 0x1234ABCDEF 45.2300 (50.1%) 100 0.4523 0 SELECT users
# Avg Min Max Err Code Query
# --- --- --- --- ----- -----
# 0.45s 0.12s 2.10s 0 0 SELECT * FROM users WHERE email = 'xxx@example.com'
怎么看?
- 重点看 Rank 和 Response time 占比最高的那几条。
Query 1占了总响应时间的50%,显然是头号嫌疑人。- 点进去看具体SQL:
SELECT * FROM users WHERE email = 'xxx@example.com' - 问题很明显:
SELECT *拿所有列,WHERE email没索引。
3. 高级用法:精准定位
按时间范围分析:
pt-query-digest --since "2023-10-01 10:00:00" --until "2023-10-01 11:00:00" slow.log
按查询类型过滤:
pt-query-digest --filter '$event->{fingerprint} =~ /SELECT.*FROM users/' slow.log
输出到文件,方便分享给同事:
pt-query-digest slow.log > slow_report.txt
生成可视化报告(HTML):
pt-query-digest --output=report slow.log
4. 常见坑:误判与漏判
坑1:参数化问题
有些SQL虽然内容不同,但执行计划一样。pt-query-digest 会自动归一化(fingerprint),把 WHERE id = 1 和 WHERE id = 2 归为一类。这是好事,但也可能掩盖问题。
比如:
SELECT * FROM orders WHERE create_time > '2023-01-01';
SELECT * FROM orders WHERE create_time > '2023-06-01';
归一化后变成:
SELECT * FROM orders WHERE create_time > ?;
如果你发现这个指纹很慢,但实际业务中只有一部分时间范围有问题,就需要进一步分析。可以用 --no-query-cache 或手动查看原始SQL。
坑2:临时表与文件排序
慢日志里可能记录的是“执行时间”,但真正的瓶颈可能是“临时表”或“文件排序”。pt-query-digest 报告会显示 Rows_examined 和 Rows_sent 的比率。
比如:
# Rows_examined: 1000000
# Rows_sent: 100
扫描了100万行,只返回100行,效率极低。这时候要看 EXPLAIN 输出,检查有没有用到索引,有没有临时表。
坑3:锁等待被误判为慢查询
如果SQL因为锁等待而变慢,慢日志记录的是“总执行时间”,包括等待时间。但实际上,查询本身可能很快,只是被锁住了。
解决方法:同时开启 engine_innodb_status 和 innodb_lock_wait_timeout 监控,结合 pt-deadlock-killer 分析死锁。
四、 PMMA:让监控变成“打游戏”
pt-query-digest 是事后分析,而 PMMA(Percona Monitoring and Management Agent)是实时监控系统。它不像 Prometheus + Grafana 那样复杂,开箱即用,适合中小团队。
1. PMMA 能监控什么?
- QPS/TPS:每秒查询/事务数
- 连接数:当前连接、最大连接
- CPU/内存/IO:服务器资源使用情况
- InnoDB缓冲池命中率:关键指标,低于95%要警惕
- 慢查询实时趋势
- 锁等待情况
- 复制延迟(主从架构)
2. 部署PMMA(简版)
# 安装 PMMA Agent
wget https://www.percona.com/downloads/percona-monitoring-and-agent/percona-monitoring-and-agent-1.2.3/binary/tarball/percona-monitoring-agent-1.2.3-xenial.tar.gz
tar -xzf percona-monitoring-agent-1.2.3-xenial.tar.gz
cd percona-monitoring-agent-1.2.3
# 配置
cp pmm-client/pmm-agent.yml /etc/pmm-agent.yml
vim /etc/pmm-agent.yml
# 修改 server 地址为你部署的 PMMA Server
# 启动
systemctl start pmm-agent
然后在浏览器访问 PMMA Server 的Web界面,就能看到一个炫酷的Dashboard。
3. 实战:如何用PMMA快速定位问题
假设你收到报警:数据库CPU飙到90%。
步骤1:打开 PMMA Dashboard,看CPU趋势图
- 找出CPU飙高的时间点,比如 14:00 - 14:30。
- 查看这段时间的 QPS 是否也飙升?如果QPS没变,CPU却高了,可能是某个复杂查询。
步骤2:查看“慢查询”面板
- PMMA会实时展示当前的慢查询。
- 如果发现某条SQL反复出现,点击进去看详情。
步骤3:结合 SHOW PROCESSLIST
- 在PMMA里直接执行SQL(部分版本支持),或者SSH到服务器执行。
- 找到对应的
Id,KILL掉(谨慎操作)。
步骤4:查看InnoDB状态
- PMMA有专门的InnoDB面板,看缓冲池命中率、行锁等待、死锁情况。
- 如果缓冲池命中率低,考虑加大
innodb_buffer_pool_size。
4. PMMA的坑:别被“炫酷”迷惑
坑1:监控数据有延迟
PMMA默认每分钟采集一次数据。如果问题是瞬间的(比如某条SQL跑了1秒就结束),可能抓不到。这时候还是要靠慢日志 + pt-query-digest。
坑2:告警太多,麻木了
PMMA可以配置告警规则。但如果你设得太敏感,每天收到几十条告警, eventually 会忽视。建议只告警关键指标:CPU > 80% 持续5分钟、慢查询数 > 10/秒、复制延迟 > 30秒。
坑3:资源占用
PMMA Agent本身也会占用少量CPU和内存。在资源紧张的服务器上,要注意权衡。
五、 终极武器:EXPLAIN + 索引优化
不管用多好的工具,最后还是要落到SQL本身。pt-query-digest 和 PMMA 帮你找到“谁慢”,接下来要搞清楚“为什么慢”。
1. 用EXPLAIN看执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 'paid' ORDER BY create_time DESC LIMIT 10;
重点看:
- type:
ALL(全表扫描)是最差的,ref、range较好,const最好。 - key:实际使用的索引。如果是
NULL,说明没走索引。 - rows:估计扫描行数。数字越大越危险。
- Extra:
Using filesort(文件排序)、Using temporary(临时表)是性能杀手。
2. 索引优化的黄金法则
- 最左前缀原则:复合索引
(a, b, c),查询条件必须是a开头,不能跳过。 - 避免
SELECT *:只取需要的列,减少IO和网络传输。 - 覆盖索引:如果查询的列都在索引里,就不用回表,速度极快。
- 区分度高的列放前面:比如
status只有几种值,区分度低,不适合做复合索引的第一列。
3. 一个真实案例
某电商系统,订单查询变慢。pt-query-digest 定位到:
SELECT * FROM orders WHERE user_id = ? AND create_time > ? ORDER BY create_time DESC LIMIT 10;
EXPLAIN 显示:type=ref, key=user_id_idx, rows=50000, Extra=Using filesort。
问题:虽然用了 user_id 索引,但还要按 create_time 排序,触发了文件排序。
解决方案:创建复合索引 (user_id, create_time)。
ALTER TABLE orders ADD INDEX idx_user_time (user_id, create_time);
再次 EXPLAIN,Extra 变成 Using index,速度提升10倍。
六、 总结:一套完整的排查流程
- 看全局:
SHOW PROCESSLIST,确认是资源问题还是SQL问题。 - 开日志:确保慢查询日志开启,
log_queries_not_using_indexes打开。 - 用工具:
pt-query-digest分析慢日志,找出TOP慢SQL。 - 实时看:PMMA监控CPU、连接、锁等待,捕捉瞬时问题。
- 深究SQL:
EXPLAIN分析执行计划,优化索引。 - 验证效果:优化后,再次用
pt-query-digest对比,确认改善。
记住,数据库优化不是一蹴而就的,而是一个持续的过程。今天解决了这个问题,明天可能又有新的慢SQL冒出来。养成定期看慢日志的习惯,比等到线上出问题了再救火要强得多。
希望这份指南能帮你少走弯路。如果还有具体场景的问题,欢迎继续交流。毕竟,每个系统的坑都不一样,踩得多了,也就成了专家。
