深夜两点,生产环境的报警群突然炸了。用户反馈页面加载慢得像在爬楼梯,监控图上QPS曲线呈断崖式下跌,CPU使用率飙到90%以上。这时候,绝大多数初级工程师的第一反应是:重启服务、加缓存、或者干脆盲目地给表加索引。但资深DBA知道,这种时候最需要的是冷静和精准——因为真正的凶手,往往就藏在那些看似无害的慢查询日志里。
慢查询日志:被低估的”黑匣子”
很多团队对MySQL慢查询日志(slow log)的态度很矛盾。一方面知道它重要,另一方面又觉得它”太原始”,收集回来一大堆数据不知道咋看。这种想法其实挺危险的。
让我先问你一个问题:你现在的慢查询日志开了吗?开了多久了?阈值设的是多少?
如果回答不上来,那这篇文章正好给你补补课。
MySQL的慢查询日志就像一个飞行记录仪,它忠实地记录下每一次超出阈值的查询。关键在于,你不仅要看它”记了什么”,还要知道它”为什么记”。
为什么我们要依赖慢日志?
想象一下,如果你开车时车出了故障,但车里的记录仪坏了,你除了猜还能做什么?数据库也是一样。没有慢查询日志,你就是在黑暗中摸索。即使有监控报警告诉你CPU高了、连接数多了,你也无法知道是什么导致了这些问题。
慢查询日志的价值在于它提供了因果关系。它告诉你是谁、在什么时候、执行了什么SQL、用了多长时间、扫描了多少行。这些信息是优化的基石。
配置慢查询日志的正确姿势
很多DBA在配置慢查询日志时都会踩坑。最常见的错误就是把long_query_time设得太大,比如1秒。你觉得1秒慢,但用户感知到的是整个页面的加载时间。如果10个查询每个都慢1秒,页面就需要10秒才能加载完。
正确的做法是:
-- 在my.cnf中配置
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.1 # 0.1秒,即100毫秒
log_queries_not_using_indexes = 1 # 记录未使用索引的查询
min_examined_row_limit = 100 # 只记录扫描超过100行的查询
为什么选0.1秒?这是一个经验值。对于大部分OLTP系统,单个查询在100毫秒以内完成是可以接受的。超过这个阈值,用户就能感受到明显的延迟。当然,具体阈值要根据业务场景调整,但绝对不要设成1秒那么宽松。
log_queries_not_using_indexes这个参数特别重要。很多慢查询之所以慢,不是因为查询本身复杂,而是因为没有走索引,导致全表扫描。开启这个参数后,所有未使用索引的查询都会被记录下来,哪怕它们执行时间很短。这能帮你发现潜在的索引缺失问题。
min_examined_row_limit则是一个更精细的过滤器。它确保只有扫描了足够多行的查询才会被记录,避免日志被大量无意义的小查询刷屏。
从原始日志到可操作的洞察
拿到慢查询日志只是第一步。原始的日志文件里通常堆积着成千上万条记录,直接看只会让你眼花缭乱。这时候,你需要工具来帮助你梳理和分析。
传统工具:mysqldumpslow的局限
MySQL自带了一个mysqldumpslow工具,很多教程都会推荐它。但说实话,这个工具用起来挺让人头疼的。它的输出格式不够直观,聚合逻辑也有限,面对复杂的查询模式时往往力不从心。
举个例子,如果你运行:
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
你只能得到执行时间最长的前10条查询的摘要。但这10条查询可能代表了不同的业务场景,你无法从中看出问题的根源。
神兵利器:pt-query-digest
这时候,Percona Toolkit里的pt-query-digest就派上用场了。这个工具简直是慢查询日志分析的神器,它能将原始的日志数据转化为结构化的、易于理解的报告。
为什么pt-query-digest这么强大?因为它不只是简单地把查询归类,它会进行指纹化处理,将相似的查询模式合并在一起,然后从多个维度进行分析。
实战:用pt-query-digest揪出真凶
假设你拿到了一份32MB的慢查询日志文件,里面包含了数千条慢查询记录。直接用pt-query-digest分析它:
pt-query-digest slow.log > report.txt
运行后,你会得到一个详细的分析报告。别急着往下翻,先看看报告的结构。这个报告按照多个维度对查询进行了聚类,包括:
- Query ID:每个查询模式的唯一标识
- Response time:响应时间统计
- Call:调用次数
- Rows:扫描行数
- Digest:查询指纹
第一步:找出”罪魁祸首”
报告中最关键的部分是”Top 10 by Elapsed”或”Top 10 by Count”。这两个视角都很重要。
按执行时间排序能帮你找到最影响系统整体性能的查询。假设你看到这样一个查询:
SELECT u.*, o.* FROM users u JOIN orders o ON u.id = o.user_id WHERE o.created_at > '2023-01-01'
这个查询的_elapsed时间占据了总慢查询时间的60%。这意味着,如果你能优化这一个查询,整体性能就会有显著提升。
按调用次数排序则能帮你发现那些”虽慢但频繁”的查询。有些查询单次执行时间不长,但因为被调用了几万次,累积的耗时非常惊人。
第二步:深入分析查询模式
pt-query-digest不仅告诉你哪些查询慢,还会告诉你为什么慢。它会分析每个查询指纹的:
- 执行时间分布:P50、P90、P99、最大值的分布情况
- 锁定时间:查询等待锁的时间
- 发送时间:服务器向客户端发送数据的时间
- 查询行数:扫描的行数和返回的行数
- 查询复杂度:是否有临时表、文件排序等
举个真实的例子。有一次我分析一个电商系统的慢查询日志,发现有一个查询占据了大量的CPU时间。通过pt-query-digest的详细分析,我发现这个查询每次执行都要扫描100万行数据,但最终只返回10条记录。这就是典型的”扫描多、返回少”问题。
进一步分析查询语句,发现WHERE条件里用了一个函数:
WHERE YEAR(order_time) = 2023
这个写法有个致命问题:MySQL对order_time字段应用了YEAR()函数,导致索引失效,必须进行全表扫描。优化方案很简单:
WHERE order_time >= '2023-01-01' AND order_time < '2024-01-01'
改成范围查询后,索引就能生效了,查询时间从3秒降到50毫秒。
第三步:识别重复和冗余
pt-query-digest还有一个强大的功能:它能识别出重复的查询模式。有时候,不同的应用代码可能执行了实质相同的查询,只是参数不同。这些重复查询会干扰你的判断。
通过--group-by参数,你可以指定按不同的维度进行分组:
# 按查询指纹分组
pt-query-digest --group-by fingerprint slow.log
# 按数据库分组
pt-query-digest --group-by db slow.log
# 按用户分组
pt-query-digest --group-by user slow.log
常见慢查询模式及优化策略
在分析了大量的慢查询日志后,我发现大多数性能问题都可以归纳为以下几类:
1. 缺少索引或索引失效
这是最常见的问题。很多开发者在写SQL时没有考虑到索引的使用,或者错误地使用了导致索引失效的写法。
典型场景:
-- 错误:在索引列上使用函数
SELECT * FROM orders WHERE YEAR(created_at) = 2023;
-- 正确:使用范围查询
SELECT * FROM orders WHERE created_at >= '2023-01-01' AND created_at < '2024-01-01';
-- 错误:隐式类型转换导致索引失效
SELECT * FROM users WHERE phone = 13800138000; -- phone是VARCHAR类型
-- 正确:保持类型一致
SELECT * FROM users WHERE phone = '13800138000';
-- 错误:前导通配符
SELECT * FROM products WHERE name LIKE '%手机%';
-- 优化:如果必须使用模糊查询,考虑使用全文索引
ALTER TABLE products ADD FULLTEXT INDEX ft_name (name);
SELECT * FROM products WHERE MATCH(name) AGAINST('手机');
2. 大表关联查询
关联查询是性能问题的重灾区。当两张大表进行JOIN时,如果关联条件没有合适的索引,或者数据量太大,查询性能会急剧下降。
优化策略:
- 确保JOIN字段上有索引
- 考虑将大表拆分为小表
- 使用覆盖索引减少回表
- 对于超大数据量,考虑分库分表
-- 优化前的慢查询
SELECT u.name, o.order_no, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 1
ORDER BY o.created_at DESC
LIMIT 100;
-- 优化方案:为orders表的user_id和status创建联合索引
CREATE INDEX idx_user_status ON orders(user_id, status);
3. 深分页问题
当数据量很大时,传统的LIMIT offset, size分页方式会有严重的性能问题。因为MySQL需要扫描并丢弃掉offset条记录。
典型场景:
-- 当offset很大时,性能急剧下降
SELECT * FROM orders LIMIT 100000, 10;
优化方案:
-- 方案一:子查询优化
SELECT * FROM orders
WHERE id >= (SELECT id FROM orders LIMIT 100000, 1)
LIMIT 10;
-- 方案二:延迟关联
SELECT o.* FROM orders o
INNER JOIN (SELECT id FROM orders LIMIT 100000, 10) AS tmp
ON o.id = tmp.id;
4. 统计查询复杂
对于需要实时统计的报表查询,如果表数据量大且查询条件复杂,性能问题很难避免。
优化策略:
- 使用物化视图或汇总表
- 考虑异步计算,将统计结果预先计算好
- 对于复杂的多维度统计,可以考虑使用OLAP引擎(如ClickHouse)
-- 原始复杂统计查询
SELECT DATE(created_at) as date, status, COUNT(*) as count
FROM orders
WHERE created_at >= '2023-01-01'
GROUP BY DATE(created_at), status;
-- 优化:创建汇总表,定期更新
CREATE TABLE daily_order_stats (
stat_date DATE PRIMARY KEY,
status TINYINT,
order_count INT,
total_amount DECIMAL(10,2),
updated_at TIMESTAMP
);
-- 定时任务更新统计表
INSERT INTO daily_order_stats
SELECT DATE(created_at), status, COUNT(*), SUM(amount), NOW()
FROM orders
WHERE created_at >= '2023-01-01'
GROUP BY DATE(created_at), status
ON DUPLICATE KEY UPDATE
order_count = VALUES(order_count),
total_amount = VALUES(total_amount),
updated_at = NOW();
-- 查询时直接从统计表读取
SELECT * FROM daily_order_stats WHERE stat_date >= '2023-01-01';
建立完整的慢查询监控体系
找到慢查询并优化它们只是第一步。一个成熟的DBA会建立一套完整的监控体系,确保问题能被及时发现和处理。
实时监控
除了慢查询日志,你还需要实时监控工具的配合。Percona Monitoring and Management(PMM)是一个很好的选择,它能提供实时的性能指标和慢查询分析。
# 使用PMM客户端收集数据
pmm-admin add mysql:metrics
pmm-admin add mysql:queries --user=admin --password=xxx
定期审查
建议每周或每两周对慢查询日志进行一次审查。使用pt-query-digest生成报告,重点关注:
- 新出现的慢查询:可能是新功能导致的性能问题
- 性能退化的查询:之前正常的查询现在变慢了
- 高频调用的慢查询:累积影响很大的查询
- 全表扫描的查询:需要检查索引是否合理
自动化告警
设置自动化的告警机制,当某些关键指标超过阈值时自动通知。比如:
- 慢查询数量突然增加
- 某个查询的执行时间超过阈值
- 索引命中率下降
# 使用crontab定期执行慢查询分析并告警
0 * * * * pt-query-digest --filter='$event->{Time} = int($event->{Time}/1000)*1000' /var/log/mysql/slow.log | mail -s "MySQL Slow Query Report" dba@example.com
一个真实的优化案例
让我分享一个真实的案例,帮助你理解整个优化流程。
背景: 某电商平台,用户反馈APP加载慢,后台监控显示MySQL CPU使用率持续在80%以上。
第一步:收集证据
通过pt-query-digest分析过去24小时的慢查询日志,发现以下情况:
- 总共记录了15000条慢查询
- 其中3个查询模式占据了80%的执行时间
- 平均每个查询扫描10万+行数据
第二步:定位问题
详细的分析报告显示,最严重的一个查询是这样的:
SELECT * FROM order_detail
WHERE order_id IN (
SELECT order_id FROM orders
WHERE user_id = 12345 AND status IN (1, 2, 3)
)
ORDER BY create_time DESC
LIMIT 20;
这个查询的问题是:
- 子查询返回了大量数据
- 外层查询没有合适的索引
ORDER BY和LIMIT在大数据集上性能很差
第三步:优化方案
- 重写查询,使用JOIN代替子查询:
SELECT d.* FROM order_detail d
INNER JOIN orders o ON d.order_id = o.order_id
WHERE o.user_id = 12345 AND o.status IN (1, 2, 3)
ORDER BY o.create_time DESC
LIMIT 20;
- 添加合适的索引:
-- 为orders表添加联合索引
ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, create_time);
-- 为order_detail表添加索引
ALTER TABLE order_detail ADD INDEX idx_order_id (order_id);
- 考虑添加缓存,对于用户的历史订单列表,可以适当缓存。
第四步:验证效果
优化后,同样的查询执行时间从3秒降低到50毫秒,CPU使用率从85%降到了45%。用户反馈的问题也得到解决。
给初学者的建议
如果你是刚接触数据库优化的新人,不要试图一次性解决所有问题。按照以下步骤逐步推进:
- 先确保慢查询日志开启并正确配置
- 学会使用
pt-query-digest分析日志 - 从最影响性能的查询开始优化
- 记录每次优化的前后对比数据
- 建立定期审查的习惯
记住,数据库优化不是一蹴而就的工作,而是一个持续的过程。今天优化的查询,可能在半年后又成了瓶颈。保持对数据的敏感,养成定期审视的习惯,这才是资深DBA和普通工程师的区别。
最后想说一句:慢查询日志不是”垃圾邮件”,它是数据库给你的最直接的反馈。学会倾听它的声音,你就能在问题变成事故之前找到并解决它们。
