凌晨两点十七分,告警电话把我从梦里拽出来。某核心订单系统的接口超时率飙升到百分之四十,数据库实例的CPU使用率拉满,连接数直接打到了上限。我登录跳板机,在MySQL里敲下一条SHOW PROCESSLIST,瞬间刷出来十几条Query_time超过二十秒的SELECT,全在查同一张订单表。当时的第一反应不是修这条SQL,而是想:为什么每次都要等人被电话叫醒了,才开始手工排查?
这就是慢查询的可怕之处。它不是第一次出现就把系统打挂,而是一点点累积——今天接口慢两百毫秒,明天慢五百毫秒,后天某条SQL突然走了全表扫描,数据库直接卡死。MySQL本身就提供了慢查询日志,绝大多数团队也开了,但真正能在一分钟内从几十GB的日志里把问题SQL捞出来的,其实没几个。不是不想查,是手工查真的太慢了。这篇就围绕“秒级定位”和“自动化分析”这两个词展开,分享我自己在线上环境压箱底的排查套路,适合被慢查询折磨过的DBA、后端开发、运维同学直接抄作业。
1. 慢查询为什么会拖垮业务:先搞懂背后的运行机制
1.1 一条SQL从快到慢的三个环节
很多人对“慢查询”的理解停留在表面:执行时间超过某个阈值的SQL就叫慢查询。这话对,但不够。要真正理解一条SQL为什么能拖垮业务,得先弄明白一条SQL在MySQL里到底经历了什么。
完整路径大致分三段。第一段是SQL解析和优化,MySQL拿到一条SQL后,先做词法语法解析,生成解析树,然后交给优化器决定执行计划——是走主键索引、二级索引,还是干脆全表扫描。第二段是执行阶段,存储引擎(InnoDB)按照执行计划去BUFFER POOL里找数据页,如果不在内存里,就要从磁盘读取,这和“能不能命中索引”“要扫描多少行”“有没有回表”直接相关。第三段是返回阶段,服务层把结果集拼接返回给客户端,这一段的开销取决于Rows_sent和网络传输。
慢查询日志里记录的Query_time,就是这三段的总耗时。所以当你看到一条SQL的Query_time是5秒,它可能是死在解析阶段,也可能是死在扫描了一百万行,还可能死在锁等待上。这就是为什么只调long_query_time阈值不够,你还需要能快速判断“到底慢在哪一环”的工具和思路。后面提到的每个脚本,本质都是在帮你把这三个环节的开销快速拆开看。
1.2 慢查询拖垮业务的典型链路
慢查询最恶心的不是“慢”本身,而是它像滚雪球一样放大故障。我给你画一条最常见的链路。
假设业务高峰期有100个并发请求,每个请求都要执行一条耗时3秒的慢查询。MySQL默认的连接池或者应用层的数据库连接池通常也就几十到两百个连接。每条连接执行SQL期间会一直被占用,3秒不释放,连接池很快就空了。新请求拿不到连接,要么排队等,要么直接报connection timeout。更糟的是,慢查询往往会伴随大量的Rows_examined,也就是扫描行数特别大,这会持续占用CPU和磁盘IO,让那些原本只需要几十毫秒的快查询也跟着变慢。快查询变慢之后又占用了更多的连接,恶性循环就这么形成了。
我遇到过一个更隐蔽的场景:一条慢查询在凌晨的定时任务里跑,耗时倒是不长,但它更新了某张核心表的一行数据,持有行锁的时间因为慢查询被拉长了。白天的业务SQL去更新同一行时,就出现了大量的Lock wait timeout exceeded报错。这种慢查询不直接体现在Query_time最长的那批SQL里,而是藏在锁等待里。所以排查慢查询时,Lock_time这个字段也必须关注,后面我写脚本时会专门带上它。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 手工排查慢查询为什么那么慢:常见做法与痛点
2.1 传统手工排查的流程回顾
先说以前大家最常用的排查流程,我自己也走过这条弯路。
收到告警或者业务反馈接口慢之后,通常的流程是:登录MySQL实例,先跑一句SHOW PROCESSLIST看看当前有没有特别耗时的会话;有的话直接KILL掉,先止血。然后找到慢查询日志的位置,grep出最近几分钟的日志片段,看一眼哪些SQL的Query_time比较大;再用这些SQL去EXPLAIN一把,分析是不是没走索引;最后改SQL或者加索引,重新执行验证,灰度上线。
这套流程听上去很完整,也在很多团队里跑了好几年。但实际操作过的人都知道,问题出在“临时抱佛脚”上。业务没问题的时候你不会去看慢查询日志,等出了问题再想看,整个上下文都是断的——你不知道这条SQL昨天的执行时间是多少,不知道它是不是从三天前就开始劣化了,更不知道它是第一次出现还是反复出现的高频SQL。
2.2 手工排查的四个硬伤
第一个硬伤是慢查询日志文件巨大。线上MySQL跑一天,慢查询日志动辄几个GB到几十个GB,用grep去搜一个时间段,光扫描文件就要一两分钟。等结果出来,业务已经被拖挂了,你是在给尸体做尸检,不是救人。
第二个硬伤是日志切换不规律。MySQL默认不按大小切割慢查询日志,如果没配mysqldumpslow之类的工具定期轮转,日志文件会越来越大。最难受的是,某些版本的MySQL在日志文件过大时,flush slow logs之前的新日志可能写不进文件,导致你定位到的时间段根本查不到数据。
第三个硬伤是手工分析SQL文本效率极低。慢查询日志里记录的SQL往往带完整的字面量,比如WHERE user_id = 123456和WHERE user_id = 654321在日志里是两条完全不同的记录,但本质是同一个模板。手工看的时候,你会被各种不同的参数值干扰,看不出“哪条SQL是真正的高频慢查询”。必须用工具按规则把SQL归一化后聚合统计,才能找到真正要优化的对象。
第四个硬伤是没有任何趋势概念。一次慢查询可能是偶发,也可能是劣化趋势的开端。手工排查只能看到“此刻最慢的那几条”,看不到“过去24小时慢查询数量的曲线”,更没法判断当前这个数量是正常水位还是异常飙升。没有基线,就没有判断依据。
这四个硬伤叠加在一起,导致手工排查通常需要十到三十分钟。而一个成熟的数据库运维体系里,从发现异常到定位到具体的SQL,耗时应该控制在秒级。下面这套脚本就是冲这个目标去的。
3. 一行脚本秒级定位:核心命令与设计思路
3.1 先确认慢查询开关和日志位置
无论脚本多华丽,前提是慢查询日志得开着。很多新手会犯的错是:日志压根没开,或者阈值设得太高,线上已经慢得不行了,日志里却干干净净。
登录MySQL执行这几条确认一下:
sql复制SHOW VARIABLES LIKE 'slow_query_log%';
SHOW VARIABLES LIKE 'long_query_time';
SHOW GLOBAL STATUS LIKE 'Slow_queries';
slow_query_log是总开关,ON表示开启。slow_query_log_file是日志文件路径,后面所有脚本都要用到这个路径。long_query_time是阈值,单位秒,超过这个时间的SQL才会被记录。建议生产环境设成1或者2,太高了会漏掉很多有优化价值的SQL,太低了日志量会很大。Slow_queries是全局状态值,表示当前累计的慢查询次数。可以连续执行两次,看差值,确认慢查询是不是正在发生。
如果用命令行直接连不上去,也可以通过系统Shell去调用MySQL客户端并读取变量:
bash复制mysql -uroot -p密码 -e "SHOW VARIABLES LIKE 'slow_query_log%'; SHOW VARIABLES LIKE 'long_query_time';"
这里多提一句:临时开启慢查询日志用SET GLOBAL slow_query_log = 'ON';就行,但注意这个设置重启后会失效。要永久开启,需要改my.cnf的[mysqld]段,加上slow_query_log = 1和long_query_time = 1,然后重启MySQL。线上实例如果无法随意重启,至少先全局开启,再配合日志轮转策略解决文件过大的问题。
3.2 秒级定位最慢SQL的一行命令
确认开关之后,核心武器来了。这是我在生产环境用得最多的一条命令,真正的一行,复制粘贴就能跑:
bash复制awk '/^# Query_time/{if($3>1) print $3, $5, $0}' /var/lib/mysql/mysql-slow.log | sort -rn | head -20
解释一下这条命令在干什么。慢查询日志里有一条一行一行的元信息,格式大概是:
code复制# Time: 2024-01-15T10:00:01.123456Z
# User@Host: app[app] @ [10.0.0.1] Id: 123456
# Query_time: 3.200000 Lock_time: 0.000102 Rows_sent: 1 Rows_examined: 5000000
awk先匹配以# Query_time开头的行,把第3个字段(也就是3.200000)取出来,判断如果大于1秒,就打印出耗时、锁时间以及整行元信息。sort -rn按数值倒序排,head -20只取前20条。
跑完你就能看到当前日志文件里单次执行时间最长的20条慢查询,并且每一次都有完整的Query_time、Lock_time和Rows_examined。注意,这里只输出了元信息行,没有直接拼接SQL文本。想连SQL一起看,用下面这个变种:
bash复制awk '/^# Query_time/{if($3>1){t=$3; lock=$5; getline; print "耗时:"t" 锁:"lock" SQL:"$0}}' /var/lib/mysql/mysql-slow.log | sort -t: -k2 -rn | head -20
这样输出的每一行同时带耗时和SQL内容,方便你快速判断哪些SQL值得进一步分析。
3.3 按聚合维度秒级定位高频慢SQL
单次最慢的SQL当然要关注,但真正会拖垮系统的往往不是单次最慢,而是高频慢SQL——单条执行只要800毫秒,远不到1秒的阈值可能不少团队都不会记录,但每秒跑上百次,积少成多,CPU和IO同样被拉爆。这类问题用上面的“最慢Top N”命令是抓不出来的。
MySQL自带的mysqldumpslow工具就是干这个的。它会把SQL里的数字参数归一化成N,把字符串参数归一化成S,从而把同一类SQL聚合到一起。常用组合:
bash复制mysqldumpslow -s at -t 20 /var/lib/mysql/mysql-slow.log
-s at表示按平均查询时间排序。-t 20表示只显示前20条。- 还可以用
-s c按执行次数排,-s t按总时间排。
跑了之后你会看到类似这样的输出:
code复制Count: 356 Time=0.86s (306s) Lock=0.00s (0s) Rows=99.0 (35244), app[app]@[10.0.0.1]
SELECT * FROM orders WHERE user_id=N ORDER BY created_at DESC LIMIT N
这行输出说明这个SQL模板被聚合成了356次,总耗时306秒,平均0.86秒一次。看到这种报告,你根本不需要一行行翻原始日志,直接就知道该优化哪条SQL。
如果服务器上没装mysqldumpslow,也可以用awk自己做聚合。下面这个稍微长一点,但效果接近:
bash复制awk '/^# Query_time/{getline; getline; sql=$0; sub(/[0-9]+/,"N",sql); cnt[sql]++; sum[sql]+=$3} END{for(s in cnt) if(cnt[s]>=5) print cnt[s]"次 总耗时"sum[s]"秒 "s}' /var/lib/mysql/mysql-slow.log | sort -rn | head -20
这段awk的逻辑是:遇到Query_time元信息行后跳过两行,取到SQL文本,把第一个数字替换成N作为key,用数组记录次数和总耗时,最后输出执行次数超过5次的SQL模板。注意,这个“把第一个数字替换成N”是极简归一化,误匹配率会偏高,但胜在零依赖,任何Linux服务器都能直接跑。
3.4 云RDS和无日志场景的替代方案
如果你的MySQL跑在云上,比如阿里云RDS、腾讯云TDSQL这类托管实例,一般是拿不到物理慢查询日志文件的。这时候从操作系统的Shell层面去跑awk就不现实了,得换一条路:通过performance_schema来查。
MySQL的performance_schema里有一张events_statements_summary_by_digest表,专门按SQL模板的摘要聚合统计。一条SQL执行的次数、总耗时、平均耗时、最大耗时、扫描行数都有,特别适合秒级定位:
sql复制SELECT
SCHEMA_NAME,
DIGEST_TEXT,
COUNT_STAR AS exec_count,
ROUND(AVG_TIMER_WAIT / 1000000000000, 3) AS avg_sec,
ROUND(SUM_TIMER_WAIT / 1000000000000, 3) AS sum_sec,
ROUND(MAX_TIMER_WAIT / 1000000000000, 3) AS max_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE DIGEST_TEXT NOT LIKE 'SHOW%'
AND DIGEST_TEXT NOT LIKE '%performance_schema%'
ORDER BY sum_sec DESC
LIMIT 20;
执行时间单位是皮秒,所以除以1e12换成秒。ORDER BY sum_sec DESC是按总耗时排,这是最贴近业务影响的一种排序方式。如果你更关心单次最慢的,可以改成ORDER BY max_sec DESC。
需要注意,这张表的统计不是全量存储的,受performance_schema_max_digest_storage或者max_digest_length等参数限制,数据会被淘汰。排查时如果看不到某个SQL,可能是它已经被挤出了统计池。但绝大多数场景下,这条SQL已经足够快速给出方向,比登录控制台一层层菜单翻要快得多。
3.5 为什么“一行脚本”而不是写一个完整程序
看到这里有人会问:既然要做自动化分析,为什么不写个Python脚本或者用现成的监控工具,非要“一行脚本”?
原因有三。
第一,零依赖。生产环境的跳板机和数据库服务器出于安全考虑,往往不能随便装软件。但awk、grep、sort、head这些基础命令是任何一台Linux机器上都有的,复制就能跑,不装任何东西,不申请任何权限。对讲效率的故障排查来说,这是最大的优势。
第二,秒级响应。Python脚本要写文件、装依赖、处理异常,再怎么快也要几十秒甚至几分钟。而一行管道命令直接对接日志流,即使日志文件有几十GB,awk用流式处理加head提前截断,通常能在几秒钟内出结果。遇到故障时,这几秒的差距就是“救火”和“分析事故报告”的差距。
第三,方便嵌入。一行命令可以顺手写进crontab,可以把输出重定向到文件,可以接mail发邮件,也可以塞进已有的监控脚本里。越短的东西越容易组合,这是命令行的哲学。
4. 自动化分析:从收到告警到生成报告
4.1 定时采集脚本的完整设计
定位一次慢查询只是第一步,真正的价值在于“周期性采集、可对比、有趋势”。我建议把上面的一行命令包装成一个采集脚本,放到crontab里定时跑,这样不用等出事故,每天都知道慢查询在怎么变化。
下面这个脚本是我在实际环境里跑过的版本,功能是:每10分钟扫描一次慢查询日志,把最近10分钟内出现的慢查询按SQL模板聚合,输出Top 10到指定目录,并追加到一个历史记录文件里。
bash复制#!/bin/bash
LOG_FILE="/var/lib/mysql/mysql-slow.log"
REPORT_DIR="/opt/slowquery_report"
LAST_POS_FILE="/opt/slowquery_report/.last_pos"
mkdir -p "$REPORT_DIR"
# 记录上次扫描到的位置,实现增量处理
if [ -f "$LAST_POS_FILE" ]; then
LAST_POS=$(cat "$LAST_POS_FILE")
else
LAST_POS=0
fi
# 当前文件大小
CURRENT_SIZE=$(stat -c%s "$LOG_FILE")
# 如果当前文件比上次位置小,说明日志被轮转过,从头开始
if [ "$CURRENT_SIZE" -lt "$LAST_POS" ]; then
LAST_POS=0
fi
# 提取新增日志
tail -c +"$LAST_POS" "$LOG_FILE" > "$REPORT_DIR/.new_slow.log"
echo "$CURRENT_SIZE" > "$LAST_POS_FILE"
# 用awk聚合统计新增的慢SQL
awk '/^# Query_time/{if($3>1){t=$3; getline; getline; sql=$0; sub(/[0-9]+/,"N",sql); cnt[sql]++; sum[sql]+=t; max[sql]=t>max[sql]?t:max[sql]}}
END{
for(s in cnt) {
printf "%d\t%.2f\t%.2f\t%s\n", cnt[s], sum[s], max[s], s
}
}' "$REPORT_DIR/.new_slow.log" | sort -k2 -rn | head -10 > "$REPORT_DIR/report_$(date +%Y%m%d_%H%M%S).txt"
很多人写慢查询采集脚本时会踩一个坑:每次都从头扫整个日志文件。日志一大,CPU和磁盘IO全被打满,反而成了新的慢查询来源。解决方案就是上面脚本里的last_pos机制,通过stat -c%s记录文件大小,用tail -c +从上次的位置开始读,实现增量扫描。这个思路在处理任何大文件日志采集时都通用。
4.2 结果入库并生成趋势报表
每个周期生成的报告文件是文本,数量一多,很难看趋势。我一直建议在采集脚本后面加一段逻辑,把聚合结果写进一张统计表。不需要上什么大数据组件,MySQL自己就行。
可以先建一张简单的表:
sql复制CREATE TABLE slow_query_daily (
id INT AUTO_INCREMENT PRIMARY KEY,
stat_date DATE NOT NULL,
stat_hour TINYINT NOT NULL,
sql_template VARCHAR(512) NOT NULL,
exec_count INT NOT NULL,
total_sec DECIMAL(10, 2) NOT NULL,
max_sec DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
KEY idx_stat_date (stat_date, stat_hour)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
然后在采集脚本末尾把Top 10逐条INSERT。建议按小时为一个聚合法度,单独跑一个定时任务去汇总slow_query_daily这张表,生成每天的慢查询汇总:哪些SQL模板全天执行了上千次、总耗时最长、哪个时段最集中。
有了这张表,后面要做可视化就简单了。可以用SELECT stat_hour, COUNT(*) FROM slow_query_daily WHERE stat_date = CURDATE() GROUP BY stat_hour拉出一个按小时分布的条形图数据,或者再加工成趋势曲线。我自己是直接把这张表接进了一个简单的图表页面,每天扫一眼,就知道今天的慢查询水位是不是相对昨天有明显上升。
4.3 与告警系统联动:把被动救火变成主动发现
脚本和报表都是“事后诸葛亮”,真正想减少事故,还得让慢查询的发现变成自动告警。不需要搞太复杂的系统,有两种轻量方案,我都试过。
方案一是直接基于定时采集脚本判断。每10分钟跑完采集后,如果发现某个SQL模板的执行次数超过50次,或者单次耗时超过5秒,就直接调用企业微信机器人或者钉钉机器人的Webhook,把Top 5的摘要推到群里。这样只要脚本在跑,慢查询一冒头,人立刻就能在手机上看到。我实际用下来,这个方案能覆盖七成的日常劣化场景。
方案二是接入统一的监控体系。如果公司的监控平台支持自定义指标上报,可以把脚本输出的数据通过curl上报到Prometheus PushGateway,然后在Grafana里配图表和告警规则。这种方式更规范,查询历史也更方便,但需要监控体系的配合,对很多中小团队来说前期成本偏高。我还是建议先把方案一跑起来,快速见效,后续再决定要不要往监控平台迁移。
5. 定位到慢SQL之后:优化与验收的实战手段
5.1 用EXPLAIN看懂执行计划
脚本定位是第一步,优化才是真正解决战斗。拿到一条慢SQL,第一件事是EXPLAIN,让它把执行计划摊开给你看。
sql复制EXPLAIN SELECT * FROM orders WHERE user_id = 123456 ORDER BY created_at DESC LIMIT 10;
输出结果里最需要盯的是三列:type、key、rows。type要看是不是ALL(全表扫描)或者index(全索引扫描),如果是这两个,说明没走在索引上。key显示实际用的索引,如果是NULL就说明压根没走索引。rows是预计扫描的行数,这个数字越大,SQL越危险。
我经常用一句话给后端同事解释这行输出:rows就像是快递员要跑多少个小区才能找到你的包裹,type则说明了他是在电话簿里直接翻到你家地址,还是挨家挨户敲门问。ALL就是挨家挨户敲门,哪怕最后找到了,过程也注定慢。
5.2 索引优化实战:从全表扫描到毫秒级返回
分享一个真实案例。某个订单后台列表页,用户一查三个月内的订单就卡死,接口耗时三秒多。用脚本捞出来是这条:
sql复制SELECT * FROM orders WHERE status = 1 AND created_at BETWEEN '2024-10-01 00:00:00' AND '2024-12-31 23:59:59' ORDER BY id DESC LIMIT 20;
EXPLAIN的结果触目惊心:type = ALL,rows = 4000000,全表四百万行硬扫。这条SQL的问题在于status = 1这个条件选择性太差,订单表里绝大部分订单都是status = 1,就算给status建索引,优化器算一下代价也会放弃。真正的筛选条件应该加在created_at上。
加了一个联合索引:
sql复制ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);
然后再次EXPLAIN,type从ALL变成了range,rows从400万降到了6000,SQL执行时间从3.2秒降到了40毫秒。一个索引,性能提升接近八十倍。
这里还有个细节:对于ORDER BY id DESC LIMIT 20这种查询,只有当status和created_at都作为等值或范围条件时,索引才能同时优化排序。如果排序字段没进索引,MySQL还得做一次filesort,性能会差很多。所以建索引不能只看WHERE条件的字段,还要看ORDER BY和GROUP BY的字段。
5.3 深分页慢查询的改写思路
还有一种特别常见的慢查询,是分页越翻越慢。排查日志里长这样:
sql复制SELECT * FROM orders WHERE user_id = 123456 ORDER BY id DESC LIMIT 100000, 20;
问题出在LIMIT 100000, 20。MySQL不是只取最后20条,而是要扫描前100020条,把前100000条都丢掉再返回。数据量一大,后面肯定慢。
改写的思路一般是两种。第一种是“延迟关联”,先用子查询只获取主键,再关联原表取完整行:
sql复制SELECT o.*
FROM orders o
INNER JOIN (
SELECT id FROM orders WHERE user_id = 123456 ORDER BY id DESC LIMIT 100000, 20
) tmp ON o.id = tmp.id
第二种是用“游标分页”,条件是WHERE id < 上一页最后一条记录的id:
sql复制SELECT * FROM orders WHERE user_id = 123456 AND id < 999999 ORDER BY id DESC LIMIT 20
游标分页在性能上是最优的,但它改变了分页的交互方式,不再支持“跳转到任意页”。如果产品经理坚持要页数跳转,就只能用延迟关联或者接受一定程度的分页深度限制。这个取舍不做开发的人很难理解,但搞技术的应该给业务说明白:无限深分页是有物理极限的,越往后越慢是必然规律。
5.4 参数调整与业务侧联合优化
有些慢查询不是SQL写得差,而是表本身太大了,或者数据库参数不合理。比如innodb_buffer_pool_size设得太小,热点数据全都挤不进内存,每次都打磁盘,SQL再快也快不起来。这个参数通常建议设为物理内存的60%到70%,但要看实例里是否还有其他大占用。
还有一类是业务侧可以优化的,比如报表类查询,根本不需要查实时数据,为什么不走从库或者离线数仓?我之前遇到过一个团队,每天在核心库上跑一张几千万行的聚合报表,白天报表一执行,核心交易接口跟着抖。后来直接把报表数据源切到了专门的只读从库,问题瞬间消失。这种优化比调任何参数都划算,因为它直接从架构层面把压力分流了。
6. 踩坑记录与注意事项
6.1 慢查询日志的经典坑
慢查询日志本身就有几个防不胜防的坑,列个表给各位提个醒。
| 常见问题 | 现象 | 解决思路 |
|---|---|---|
| 日志文件无限增长 | 磁盘占用告警,甚至写满导致数据库只读 | 配置定时切割,用logrotate或脚本做mv + FLUSH LOGS |
long_query_time设得太小 |
日志里塞满了各种几十毫秒的SQL,真正该看的被淹没 | 先设2秒跑一周,根据日志量逐步下调 |
log_queries_not_using_indexes误开 |
每条没走索引的SQL都被记录,哪怕它只有1毫秒 | 确认是否需要全局开启,最好只在排查期临时开 |
日志被手工删除但没有FLUSH LOGS |
数据库仍持有删除文件的句柄,磁盘空间不释放 | 删除日志后必须执行FLUSH SLOW LOGS或者重启实例 |
| 时区问题 | 日志记录时间和实际业务时间差8小时,按时间排查对不上 | 统一确认MySQL的时区设置,脚本里做时间偏移处理 |
FLUSH SLOW LOGS这个坑很多人踩过。你用rm -rf mysql-slow.log把日志删了,以为磁盘空间会回来,结果数据库进程还开着旧文件句柄,空间一点没释放。正确做法是重命名日志文件,然后执行FLUSH SLOW LOGS让数据库重新创建新文件,再由外部任务删除历史文件。
6.2 权限和安全方面的经验
跑慢查询分析脚本时,涉及的系统权限和数据库权限要提前规划好。SHOW VARIABLES和SHOW GLOBAL STATUS只需要普通账号就能执行,但读物理慢查询日志需要操作系统层面的文件读权限,不是所有DBA账号都有。云数据库场景下,通常只能通过控制台或者performance_schema查询,权限模型不一样。
另外,脚本里如果硬编码了数据库密码,注意不要提交到代码仓库,也不要放在普通用户可读的地方。可以用环境变量或者配置文件方式加载,并设置chmod 600权限。还有一点:不要在生产环境直接对慢查询日志执行rm -rf,哪怕你是根用户。先确认没有进程正在占用,再决定要不要轮转。
6.3 不同环境的适配:Docker、Windows、多实例
如果MySQL跑在Docker容器里,慢查询日志文件通常存在宿主机挂载的卷里。脚本里LOG_FILE的路径要用宿主机上的实际路径,而不是容器内的路径。可以先docker exec mysql ls -l /var/lib/mysql/mysql-slow.log查一下挂载关系,再调整LOG_FILE变量。
开发环境的Windows用户,用mysqldumpslow不太方便,建议在WSL或者Git Bash里跑awk命令,或者直接用MySQL Workbench看慢查询日志管理面板。如果只有一个MySQL实例,脚本简单得多;如果是多实例,比如一主多从,每台机器都要跑采集脚本,建议在输出文件名和报表字段里加上实例标识,否则汇总的时候你根本分不清哪条SQL是哪个实例上跑的。
还有一个小细节:有些MySQL版本(尤其是8.0)的日志格式和5.7版本有差异,比如时间戳格式从Y-m-d H:M:S变成了YYYY-MM-DDTHH:MM:SS。awk里按空格取字段的索引可能略有变化,跑通之前先手动执行一次head -5看看日志格式再动手。这个习惯能帮你节省大量调试时间。
7. 可视化与长期运维:把策略沉淀成体系
7.1 每日慢查询邮件报告
定时采集做了,趋势表有了,下一步是让它变成每天自动发给团队的“健康日报”。我记得最土也最有效的做法是,写一个Shell脚本在每天早上9点跑一次SQL汇总,把结果用mailx发到大家的邮箱。
bash复制#!/bin/bash
DATE=$(date -d "yesterday" +%Y-%m-%d)
mysql -h127.0.0.1 -uroot -p密码 slow_db -e "
SELECT sql_template, SUM(exec_count) AS total_cnt, ROUND(SUM(total_sec),2) AS total_time, ROUND(SUM(total_sec)/SUM(exec_count),2) AS avg_sec
FROM slow_query_daily
WHERE stat_date = '$DATE'
GROUP BY sql_template
ORDER BY total_time DESC
LIMIT 15;
" | mailx -s "MySQL慢查询日报 $DATE" dba@example.com
不要小看这种“笨办法”。一份每天自动发出来的日报,能倒逼团队养成每天看慢查询的习惯,很多问题在还只是“苗头”的时候就被发现了。相比复杂的可视化大屏,这个方式成本最低、见效最快。
7.2 接入Grafana的可视化方案
如果团队里有现成的Prometheus和Grafana,建议把数据往Grafana里送。我之前用一个很简单的方式实现了联动:定时采集脚本通过curl把指标POST到Prometheus的PushGateway,指标名就叫mysql_slow_query_exec_count,label带上SQL模板的哈希值和实例名,然后在Grafana里创建一个按模板聚合的柱状图和趋势图。
效果很直观:哪天哪个接口新引入了一条慢SQL,图上会多出一根很高的柱子,一点都不需要猜。告警规则也写在Grafana里,比如“单模板执行次数超过100次/小时”就触发警告,“平均耗时超过3秒”就触发严重告警。这套体系跑起来后,我的值班电话少了很多。
7.3 建立慢查询治理的闭环
最后想强调一点:慢查询治理不是一次性的,而是一个闭环。定位、优化、验证、监控,这四个环节缺一不可。定位靠脚本和工具;优化靠索引改写和架构调整;验证靠EXPLAIN和线上对比;监控靠自动化的告警和日报。这四个环节串起来之后,慢查询的治理才能从“救火”变成“防火”。
我给团队的长期维护建议是:每两周看一次慢查询日报的趋势,每月做一次慢查询治理专项,把那些反复出现的高频慢SQL按业务优先级排期处理。同时维护一个“慢查询治理台账”,记录每条SQL的发现时间、原因分析、优化方案和理解结果,避免同一个坑踩两次。
根据我个人的实际体会,慢查询排查最难的其实不是技术,而是“从哪下手”的决策。有了高效的定位脚本,有了自动化的分析流程,有了可视化的趋势报表,整个决策链条才完整。这套东西搭起来之后,哪怕某天凌晨三点又被电话叫醒,我也不用再紧张了,因为我知道脚本会告诉我答案。
