1. 慢查询日志到底解决了什么问题:先搞懂它是给谁看的
做MySQL性能调优这些年,我最常被问到的一句话是:“数据库明明没报错,但线上接口就是慢,到底该怎么查?”大多数人的第一反应是看监控、看CPU、看连接数,但真正能一针见血定位到“是哪条SQL拖垮了整体性能”的,往往就是慢查询日志。
慢查询日志(Slow Query Log)是MySQL提供的一种日志能力,专门用来记录执行时间超过指定阈值的SQL语句。它的定位非常明确:不是帮你分析全部SQL,而是帮你圈定那些“异常慢”的语句。你可以把它理解成一个“差生记录本”——只有表现不达标的才会被点名,正常执行的SQL一概不记录。这种机制的好处是成本可控、噪音小,让排查问题的人把注意力集中在真正需要优化的少数SQL上。
在正式使用之前,我建议你先分清楚慢查询日志和通用查询日志(General Query Log)的区别。通用查询日志记录所有SQL,执行量大、文件膨胀快,一般生产环境根本不敢开;而慢查询日志只记录超时SQL,配合合理的阈值设置,性能和存储开销都可以接受。我见过不少团队因为分不清这两者,直接把通用日志开了两天,结果磁盘被几GB的日志文件塞满,教训相当深刻。
慢查询日志的适用场景非常广,但最典型的有三类:
- 线上接口偶发变慢,但常规监控看不出规律,需要定位具体SQL;
- 新上线的查询功能存在全表扫描或索引失效问题,需要快速筛查;
- 大促或高峰期的数据库压测,需要找出耗时TOP N的语句逐一优化。
无论你是后端开发、DBA还是运维,只要你的应用接入了MySQL,慢查询日志就是排查SQL性能问题时最该先看的东西。下面我从原理到配置、再到实战分析,把完整链路捋一遍,最后会给出我的踩坑经验。
2. 从参数到文件:一步步把慢查询日志正确打开
2.1 先看当前状态,别急着改配置
慢查询日志默认是关闭的,但不同的发行版、不同的云数据库,默认行为可能不一样。接到一台陌生的数据库,我建议你第一步先执行这条SQL,把现状摸清楚:
SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time'; SHOW VARIABLES LIKE 'slow_query_log_file';slow_query_log是总开关,ON表示已开启;long_query_time是阈值,单位是秒,默认通常是10.000000;slow_query_log_file是日志文件的存放路径。另外还有一个容易被忽略的变量log_queries_not_using_indexes,它控制的是“没有走索引的查询是否也记入日志”。
很多人只看前三个变量,漏掉了log_queries_not_using_indexes。我自己的习惯是把它打开,因为全表扫描的SQL哪怕执行时间很短,在数据量增长后也会变成定时炸弹。提前记录这些语句,等于在问题爆发之前就拿到了排查线索。不过要注意,开启这个开关后日志量会明显增加,需要配合日志轮转策略一起考虑。
2.2 动态开启:不重启数据库的热修改方式
MySQL对慢查询日志的参数支持动态修改,不需要重启实例,这在大版本升级或线上紧急开启时非常方便。命令很简单:
SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; SET GLOBAL log_queries_not_using_indexes = 'ON';执行完再查一下SHOW VARIABLES LIKE 'long_query_time',你会发现当前会话里看到的可能还是旧值。这是因为long_query_time这个参数同时存在全局值和会话值,新设置只对后续新建的连接生效,当前连接读到的仍然是会话级别的旧参数。这个细节坑过不少人——改完参数测试不生效,以为没改成功,其实换个新连接再验证就正常了。
关于阈值设多少,我的经验是分环境区别对待:
| 环境 | 推荐阈值 | 说明 |
|---|---|---|
| 开发环境 | 0.1 ~ 0.5秒 | 尽量抓全慢SQL,宁可多记不可漏记 |
| 测试环境 | 1秒 | 结合数据量模拟线上压力 |
| 生产环境 | 1 ~ 2秒 | 避免日志量过大,同时覆盖大多数慢查询 |
阈值也不是一成不变的,如果系统整体响应本来就很快、P99延迟在200毫秒以内,那生产环境用1秒阈值能筛出不少“偷偷变慢”的语句;如果业务本来就重、接口普遍在800毫秒上下,阈值设在2秒更合适,否则日志量会大到失去重点。
2.3 持久化配置:让修改在重启后依然生效
动态设置有个致命弱点——数据库一重启,所有GLOBAL参数全部还原。要让配置永久生效,必须修改MySQL配置文件。以最常见的my.cnf(Windows下是my.ini)为例,在[mysqld]段下添加:
[mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 1 log_queries_not_using_indexes = 1这里有个容易踩的坑:slow_query_log_file指定的目录必须存在,而且MySQL的运行用户(通常是mysql用户)对这个目录要有写权限。如果你配的路径目录不存在,MySQL启动时会直接报错,或者日志被写入系统默认路径,让你找半天找不到日志在哪。稳妥的做法是先手动创建目录并授权:
mkdir -p /var/log/mysql chown mysql:mysql /var/log/mysql chmod 755 /var/log/mysql修改完配置文件后重启MySQL服务,再确认参数是否生效。
2.4 一个被我反复强调的冷知识:还有表形式可以选
慢查询日志不仅可以写文件,MySQL还支持把日志写入mysql.slow_log这张系统表。这个思路在云数据库上特别实用,因为云环境往往不让你直接访问文件系统,写表之后用SQL就能查询和统计。
开启表记录的方式是设置log_output参数:
SET GLOBAL log_output = 'TABLE'; SET GLOBAL slow_query_log = 'ON';然后直接在表里查:
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;表记录和文件记录可以二选一,也可以同时启用(log_output = 'TABLE,FILE')。但我个人建议生产环境还是以文件为主,因为文件格式对后续用mysqldumpslow、pt-query-digest这类工具做统计更加友好;表记录适合临时排查,或者当你确实没法访问日志文件时作为替代方案。另外,slow_log表会自动记录SQL执行时间、锁等待时间、扫描行数等关键字段,信息量比默认文件格式更结构化,做临时分析时反而更直观。
3. 拿到日志先别看内容:mysqldumpslow的正确打开姿势
3.1 为什么不用cat直接翻日志
慢查询日志默认会记录完整的SQL文本,如果一个慢SQL在业务里被调用了上万次,日志里就会有上万条几乎一样的记录。直接cat日志文件去人肉翻,等于把自己埋进重复信息里,根本抓不住重点。
正确的方式是用MySQL自带的日志分析工具mysqldumpslow。它在MySQL安装目录的bin目录下,不需要额外安装。这个工具的核心能力是把结构相似的SQL抽象成模板,然后按指定维度做汇总统计。它的用户就是那些被慢SQL折磨的开发者和DBA——让你从几万行日志里快速提炼出真正需要优化的SQL模式。
3.2 最常用的一组命令,我直接给你
以下命令是我日常排查慢查询日志时的标准动作:
# 按平均查询时间排序,显示前10条 mysqldumpslow -t 10 /var/log/mysql/mysql-slow.log # 按总执行时间排序 mysqldumpslow -s t /var/log/mysql/mysql-slow.log # 按执行次数排序 mysqldumpslow -s c /var/log/mysql/mysql-slow.log # 按扫描行数排序 mysqldumpslow -s r /var/log/mysql/mysql-slow.log # 输出完整SQL语句(不加-n或加-a) mysqldumpslow -a -t 10 /var/log/mysql/mysql-slow.log # 只显示包含特定表名或条件的SQL mysqldumpslow -g "user_order" /var/log/mysql/mysql-slow.log几个关键参数说明:
-s:排序字段。c代表执行次数,l代表锁等待时间,r代表扫描行数,t代表执行时间。按不同维度排序能看到完全不同的结论——执行次数多的SQL可能是高频小慢,单次时间长的SQL可能是低频大慢,两类问题优化思路完全不同。-t:返回前N条,类比LIMIT N。-g:正则匹配,只统计包含指定关键字的SQL。-a:默认情况下工具会把SQL中的数字参数替换成N做聚合,加-a表示显示原SQL文本。
3.3 聚合逻辑:为什么数字会被替换成N
mysqldumpslow默认会把SQL里的具体数值替换成N,比如:
SELECT * FROM orders WHERE user_id = 12345 AND status = 1;会被归一化成:
SELECT * FROM orders WHERE user_id = N AND status = N;这个设计很聪明,因为WHERE条件里的具体值不影响SQL执行计划类型——只要user_id列上有没有索引,对12345和对67890来说执行成本是一样的。把具体值抽象成N之后,原本因为参数不同而要统计几万次的SQL就能归类成一条模板,统计结果才有意义。这也是慢查询日志分析的核心思想:找模板,而不是找单条语句。
如果你确实想看某条SQL的具体参数值,可以加-a参数,但输出量会大很多,一般是确认“这条SQL需要复现”时才用。
3.4 输出结果里的每个数字都别放过
mysqldumpslow输出的每一行都包含几个关键指标,以这行为例:
Count: 4825 Time=2.31s (11154s) Lock=0.50s (2413s) Rows=475319.5 (2293692920), vuser[ vuser]@[10.10.10.10]Count: SQL模板在日志记录周期内出现的次数,4825意味着这条SQL被慢速执行了4825次。Time=2.31s: 平均执行时间2.31秒,括号里的11154s是这4825次累计消耗的总时长。Lock: 平均锁等待时间,括号内是累计值。锁等待过高说明并发场景下有锁竞争,要重点看表锁或行锁的使用是否合理。Rows: 平均扫描行数475319.5,括号内是累计扫描行数。这个数字要和Time一起看——如果扫描行数高但执行时间不算太长,说明SQL扫描了大量数据但没做复杂计算;如果执行时间也高,大概率是缺少有效索引导致全表扫描。
我的判断经验是:优先看Rows,扫描行数和实际返回行数的差距越大,索引设计就越值得怀疑。比如一条SQL扫描47万行只返回100行,基本可以确定索引没有正确命中。
4. 一个真实慢SQL的分析过程:从日志到索引优化
4.1 先复现问题,再动手改
我曾经接手过一个订单查询接口的优化,现象是每天下午高峰期接口变慢,偶发超时。因为线上不敢随便乱动,我先把慢查询日志打开,阈值设成1秒,观察了24小时,再用mysqldumpslow -s c -t 5统计出最频繁的慢SQL。
结果显示排名第一的SQL模板长这样:
SELECT * FROM orders WHERE user_id = N AND status = N ORDER BY create_time DESC LIMIT N;平均执行时间2.31秒,累计执行4825次,平均扫描47万行。一眼就能看出问题:查询条件带user_id和status,排序字段是create_time,如果索引设计不合理,排序会走filesort,大量扫描行又来自联合索引失效或缺失。
我先在测试库上复现了这个SQL,然后查看执行计划:
EXPLAIN SELECT * FROM orders WHERE user_id = 12345 AND status = 1 ORDER BY create_time DESC LIMIT 10;执行计划里type显示为ALL,possible_keys为空,rows预估48万。这就是典型的全表扫描没有命中任何索引。当时表上只有一个主键和user_id单列索引,但status条件加上ORDER BY create_time的组合让优化器放弃了单列索引,直接走全表。
4.2 我的修复方案:联合索引的字段顺序有讲究
我给orders表加了一个联合索引:
ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, create_time);加索引的原理是:联合索引的B+树会按照(user_id, status, create_time)的顺序排列数据,查询条件用user_id和status等值匹配时能快速定位,排序又恰好命中了索引的有序性,绕开了filesort。
但索引字段的顺序不是随便排的。联合索引遵循最左前缀原则:查询条件里的字段和排序字段,顺序要配合好。如果把create_time放在第二列、status放在第三列,那WHERE user_id = N AND status = N就只能用上user_id一个字段做过滤,status上的过滤直接失效,效果大打折扣。所以等值条件在前、排序字段在后的排列方式,是应对“过滤+排序”场景的通用套路。
优化后我又查了一次执行计划,type变成了ref,rows降到800多,慢查询日志里这条SQL彻底消失。整个排查过程从开日志到完成优化,用时不到半天。
4.3 优化完别忘了对比验证
加完索引不是终点,一定要用数据说话。我每次优化完,会对比三个指标:
- 优化前后同SQL在测试库的执行时间;
- 优化后24小时内慢查询日志里同模板SQL的出现次数;
- 线上接口的P99耗时变化。
如果只盯着SQL执行时间快不快,忽略整体接口表现,很容易出现“单条SQL优化完了,但业务依然慢”的情况——因为瓶颈可能在应用层、网络层或者其他SQL上。慢查询日志是定位SQL问题的入口,但优化效果必须在业务层面验收。
5. 这些慢查询日志的坑,我几乎都踩过
5.1 阈值设不对的翻车现场
我刚带团队那会儿,图省事把生产环境的long_query_time设成0.5秒,结果业务高峰一个小时的慢查询日志就飙到1.8GB,直接把磁盘占满了。后来我总结出规律:阈值不是越低越好,而是要考虑三个因素——业务接口可接受的耗时、SQL总执行量、日志磁盘空间。
一个稳妥的启动方案是先设2秒跑一周,看看慢SQL的量级和分布,再根据“需要发现的最慢SQL”和“可容忍的日志量”这两个边界慢慢往下调。调整阈值后至少观察24小时再决定下一个目标值,不要一天之内反复横跳。
5.2 日志文件不轮转,磁盘迟早要爆
MySQL官方并不默认帮你管理慢查询日志的轮转。日志文件会持续增长,直到你把服务重启或者手动处理。这带来的隐患除了磁盘空间,还有一个容易被忽略的问题——日志文件越大,mysqldumpslow扫描和解析的时间越长,分析效率直线下降。
Linux下最简单的方案是用logrotate做轮转。创建一个配置文件,比如/etc/logrotate.d/mysql-slow,内容大致可以这么写:
/var/log/mysql/mysql-slow.log { daily rotate 7 compress delaycompress missingok notifempty create 640 mysql mysql }含义是每天轮转一次,保留7天,旧日志压缩,创建新日志文件时指定用户和权限。还有个细节必须提醒:轮转之后,MySQL可能还握着旧文件的句柄继续写,需要刷新日志让MySQL重新打开新文件。不同版本做法不一样,MySQL 8.0可以用FLUSH LOGS;来刷新,执行后会重新打开当前的日志文件,避免日志继续写入已被轮转走的旧文件。
5.3 开启慢日志不等于万事大吉
有些团队把慢查询日志打开后就再也不看,过期日志堆成山,真正出了问题也不知道去分析。日志是给人看的,不是给磁盘增加负担的。我建议形成固定节奏:至少每周用mysqldumpslow做一次统计,把Top 5慢SQL的优化状态跟踪起来。甚至可以写一个简单的脚本,每天定时生成当日慢SQL统计并推送给自己,让问题在爆雷之前就被发现。下面是个简单的Shell脚本思路:
#!/bin/bash LOG=/var/log/mysql/mysql-slow.log OUT=/tmp/slow_report.txt /usr/bin/mysqldumpslow -s t -t 5 $LOG > $OUT再加到crontab里定时执行,配合监控告警就形成了一个基础的慢SQL巡检机制。
5.4 线上开启别选错时间窗口
这个坑是我在一次大促前踩的。当时为了排查问题,我在白天业务高峰期直接执行SET GLOBAL slow_query_log = 'ON',结果日志写入带来的额外I/O让本就紧张的数据库负载雪上加霜。慢查询日志的写入虽然不像通用日志那样频繁,但日志量一多,I/O开销依然存在。后来我养成一个习惯:需要临时开启线上慢日志时,选在业务低峰期操作,比如凌晨;如果没有低峰期,比如24小时都有流量,那就优先用TABLE方式记录,因为表写入的I/O模式相对可控。
6. 慢查询日志和EXPLAIN、性能分析工具的组合用法
6.1 慢日志负责“找题”,EXPLAIN负责“解题”
慢查询日志的职责边界很清晰:帮你圈定可疑SQL,但它本身不告诉你SQL为什么慢。想知道为什么慢,必须配合EXPLAIN看执行计划。流程通常是这样:拿慢日志里统计出的Top SQL模板,替换成真实参数后在测试库执行,然后前面加EXPLAIN,重点看四个字段:
type:访问类型。从好到差依次是const>eq_ref>ref>range>index>ALL。看到ALL基本就是全表扫描,优先考虑补索引。key:实际使用的索引。如果是NULL,说明优化器没选到索引。rows:预估扫描行数。这是一个非常直观的“危险信号”。Extra:如果出现Using filesort或Using temporary,说明排序或去重操作在临时表里完成,性能大概率不理想。
6.2 进阶技巧:用pt-query-digest做深度分析
自带工具之外,想要更专业的统计和可视化,可以考虑Percona Toolkit里的pt-query-digest。它对慢查询日志的解析维度比mysqldumpslow丰富很多,不仅按时间分布、执行频率汇总,还能生成一份HTML报告,对每条SQL给出响应时间占比、执行次数、扫描行数等二十多项指标。
基本用法:
pt-query-digest /var/log/mysql/mysql-slow.log > report.txt pt-query-digest /var/log/mysql/mysql-slow.log --limit 10它的输出里有“Profile”和“Query Report”两个核心板块。Profile按总响应时间倒序排列了所有SQL模板,一眼就能看到TOP N的占比;Query Report则展示每条SQL的执行计划归纳、出现的次数、平均耗时等明细。用惯了之后,我甚至觉得它就是慢查询日志分析的“标配”,但工具毕竟是外部依赖,如果你只想用MySQL官方能力,mysqldumpslow已经能覆盖八成以上日常需求。
6.3 最终闭环:优化SQL还是调整表结构
分析慢SQL,最终落到两个方向:改SQL写法,或者改表结构。到底改哪个,我有一套判断逻辑:
| 现象 | 优先动作 |
|---|---|
| WHERE条件能用索引但没建索引 | 加索引 |
| 索引已建但优化器不走索引 | 检查数据分布、索引基数、是否有函数包裹列 |
| SELECT * 取了很多不需要的列 | 改成只取必要字段,减少回表 |
| 大页LIMIT分页扫描深 | 用游标分页、延迟关联等方案替换 |
| JOIN关联字段类型不一致 | 统一字段类型,保证索引可命中 |
| 排序字段和索引顺序不匹配 | 调整索引字段顺序,消除filesort |
这套判断逻辑不是凭空来的,它对应的是B+树的查找机制和优化器的选择策略。比如WHERE DATE(create_time) = '2024-01-01'这种写法,即使create_time上有索引,函数包裹会让索引失效,因为优化器无法对函数处理后的结果做范围匹配。这种属于SQL写法问题,改写法比改结构更合理。
7. 结尾一点小补充:把慢查询日志用成数据库巡检习惯
分享一个我现在的日常做法,供你参考。每次接手一个新项目或者新数据库,我做的第一件事不是看表结构,而是先看慢查询日志是否存在、是否开启、阈值是多少。如果没开,就选个低峰期配上合适的参数;如果开了,就先把mysqldumpslow -s r -t 10的结果跑一遍,摸清全表扫描的底数。这个习惯帮我排查过太多隐蔽问题,也帮同事解决过不少“玄学变慢”的case。
最后再提醒一句:慢查询日志不是银弹,它只是把SQL性能问题的“候选人”筛出来,真正的优化动作还是要回到执行计划、索引设计、SQL写法上去。但它确实是所有手段里成本最低、见效最快的第一步。希望你用完今天这套方法后,再遇到“线上突然变慢”的情况,能不再靠猜,而是打开日志、跑一把mysqldumpslow,快速锁定真凶。