搞MySQL性能排查的人,迟早都会跟慢查询日志打交道。我之前排查过一个持续了一整天的接口超时问题,最后定位到是一句没走索引的UPDATE把整库拖住了。当时要是没开着慢查询日志,光靠猜,不知道要折腾多久。所以说慢查询日志是MySQL性能分析里最便宜、最值得先开的一项能力,一点不过分。
这篇不是讲理论,我直接把慢查询日志从“是什么”到“怎么开”,再到“拿到日志之后怎么做优化”这条线完整带一遍。适合刚接触MySQL优化的人,也适合已经开了慢日志但不知道怎么用的人。全文以MySQL 5.7和8.0为主,命令和配置都给你可以直接抄的版本。
1. 慢查询日志:一条SQL从“慢”到“被发现”的完整旅程
1.1 慢查询日志到底记录了什么
慢查询日志(Slow Query Log)是MySQL官方提供的、用来记录执行时间超过指定阈值的SQL语句的日志。默认阈值是10秒,也就是说一条SQL执行时间超过10秒,才会被写入慢日志文件。
这个描述看着简单,但很多人理解有偏差。我拆开说。
第一,它记录的是“执行完成”的语句。如果一条SQL因为锁等待一直卡住,还没执行完,那它暂时不会出现在慢日志里,等它执行完、超时、或者被kill掉之后,才可能被记录。这里有个容易忽略的点:一条SQL的“执行时间”是从服务器收到请求开始,到返回结果结束的整个过程,不是单纯的计算时间。所以慢日志里的Query_time实际上包含了解析、优化、执行、发送结果的全过程。
第二,它不只记录SELECT。UPDATE、DELETE、INSERT这类写操作如果很慢,同样会记录。很多人只盯着慢查询日志里的SELECT,结果把慢UPDATE漏掉了,这不对。
第三,默认情况下,它不记录没走索引的查询,除非你额外开启log_queries_not_using_indexes参数。这个参数我建议在开发环境打开,生产环境要慎重,因为一旦打开,日志量会暴涨,后面我会专门讲。
1.2 为什么每个后端和DBA都应该先看它
MySQL里能观察SQL执行情况的手段不少,比如performance_schema、sys schema、general log、慢查询日志。但慢查询日志在实际排查中的优先级非常高,原因有几个。
一是成本低。开慢日志对线上的性能影响很小,几乎可以忽略,特别是用FILE输出模式的时候。它不像general log那样记录所有SQL,不会产生海量写入,也不会明显拖慢数据库。
二是信息足够定位问题。一条慢日志记录里包含了执行时间、锁等待时间、扫描行数、返回行数、执行SQL原文。有了这些信息,你基本能判断这条SQL的问题是出在没走索引、扫描行数过大、还是锁等待太久。
三是不依赖外部工具。官方自带的mysqldumpslow就能做初步聚合,不用装额外的监控系统。很多中小团队没有性能监控平台,慢日志就是最朴素的“监控系统”。
1.3 慢日志和“死锁”“阻塞”到底是什么关系
热词里有个“慢查询 死锁 语句阻塞”,很多人会混在一起。这里先说清楚:慢查询日志记录的是“某条SQL执行得慢”,而死锁和阻塞是“SQL执行慢的两种可能原因”。
死锁本身不会直接写进慢查询日志,它是两个或多个事务互相持有对方需要的锁,导致谁也走不下去。MySQL检测到死锁后,会回滚其中一个事务,让另一个继续。这个“被牺牲”的事务,如果执行时间超过long_query_time阈值,它对应的SQL的慢日志记录里,Lock_time和Query_time通常会异常偏高。
阻塞就更直接了。事务A锁住了某一行,事务B去更新同一行,B只能等A提交或回滚。如果A一直不提交,B就一直在等锁,直到innodb_lock_wait_timeout(默认50秒)报错。这种情况下,B的SQL执行时间会超过阈值,被记进慢日志,而且Lock_time会非常显眼。
所以我的排查习惯是:先看慢日志里的Lock_time,如果Lock_time占比高,那基本可以断定问题不是SQL本身慢,而是并发锁冲突。这个思路后面第五部分会用一个完整案例展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 配置前必修课:慢查询日志的核心参数与版本差异
2.1 核心参数逐个拆解
配置慢查询日志之前,先把参数搞明白。下面这几个是最常用的,我按重要程度排个序。
| 参数名 | 默认值 | 作用 | 是否动态生效 |
|---|---|---|---|
| slow_query_log | OFF | 慢查询日志总开关,1或ON表示开启 | 是 |
| slow_query_log_file | 主机名-slow.log | 慢日志文件路径 | 是(需确认文件可写) |
| long_query_time | 10 | SQL执行时间超过该值才记录,单位秒,可设小数如0.1 | 是,但已建立的连接不生效 |
| log_queries_not_using_indexes | OFF | 记录所有未使用索引的SQL,即使执行很快 | 是 |
| min_examined_row_limit | 0 | 扫描行数小于该值的SQL不记录,用于过滤小查询 | 是 |
| log_throttle_queries_not_using_indexes | 0 | 每分钟最多记录多少条未使用索引的SQL,0为不限 | 是 |
| log_output | FILE | 日志输出方式:FILE(文件)、TABLE(表)、FILE,TABLE(都写) | 是 |
这里重点解释两个坑。
第一个是long_query_time。这个参数是支持小数的,所以在MySQL 5.7以上版本里,你可以设置成0.5、0.3甚至0.1,用来抓那些“不是特别慢,但积累起来很要命”的SQL。但是注意,它是按秒算的,不是毫秒。设置long_query_time=0.1表示100毫秒,不是1毫秒。
第二个是“已建立的连接不生效”。这句话很关键。你执行SET GLOBAL long_query_time=1之后,已经存在的数据库连接用的还是旧值,只有新建立的连接才用新值。所以实际生效之后,建议你重新连一下客户端再验证,否则你会看到“我改了怎么没反应”的假象。
2.2 MySQL 5.7 与 MySQL 8.0 的差异点
MySQL 8.0相对5.7,在慢日志方面有几个差异值得注意。
第一,默认值层面,两者默认都是关闭慢日志,但文件路径规则稍有不同。5.7默认写到datadir下的主机名-slow.log,8.0也一样,不过8.0的安装包多了一些sys schema的辅助视图,官方推荐通过sys.schema_unused_indexes这类表辅助定位索引问题,慢日志本身还是那套逻辑。
第二,MySQL 8.0支持SET PERSIST,可以把参数持久化到mysqld-auto.cnf文件里,重启后依然生效。MySQL 5.7只能用SET GLOBAL临时设置,然后改my.cnf,不然一重启就丢。这个差别在生产环境很重要。
第三,8.0对权限体系更严格。5.7里需要SUPER权限才能改全局变量,8.0改成了SYSTEM_VARIABLES_ADMIN权限。普通开发账号一般是改不了的,得DBA来操作。
第四,8.0里如果使用log_output=TABLE,查询mysql.slow_log表时,sql_text字段默认是utf8mb3字符集,如果SQL里有特殊字符,可能出现显示异常,5.7里也有类似问题但没这么扎眼。
2.3 只开日志不调参数,等于没开
我见过不少团队,打开slow_query_log之后发现慢日志里什么都没有,就以为线上很健康。真相往往不是没有慢SQL,而是long_query_time默认10秒太高了。
大多数互联网业务,查询超过1秒就值得关注,超过3秒基本就是在报警边缘。所以我一般建议:
- 开发环境:long_query_time设为0.1,再开启log_queries_not_using_indexes,尽可能暴露所有潜在问题。
- 生产环境:从1秒开始,稳定后再考虑0.5秒。不要一上来就设0.1,否则日志量和写入压力会翻几倍。
这个“先1秒,再逐步收紧”的思路,比一次性抓得很细更实用,也更好交代。
3. 从零开启慢查询日志:本地MySQL与Docker容器的完整操作
3.1 本地安装场景的配置步骤
本地装了MySQL,想开慢日志,最简单的做法是改my.cnf(Linux)或my.ini(Windows)。
在[mysqld]段落下面加这几行:
ini复制[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 0
min_examined_row_limit = 100
解释一下这几项。
slow_query_log = 1是总开关。slow_query_log_file指定日志文件绝对路径。注意这个路径要保证MySQL进程有写权限,我踩过一次坑:路径写对了,但目录属主是root,MySQL根本写不进去,启动时还不报错,只是在执行慢SQL时悄悄不写日志,排查了半天。
long_query_time = 1是阈值。log_queries_not_using_indexes建议先保持0,等确认日志量可控再开。min_examined_row_limit = 100的意思是,扫描行数低于100的SQL不记录,这个能过滤掉一堆扫描少量行但不走索引的琐碎查询。
改完配置,重启MySQL:
bash复制systemctl restart mysqld
然后登录MySQL确认参数:
sql复制SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
如果看到slow_query_log为ON,long_query_time为1,就说明开好了。
3.2 Docker容器中的MySQL怎么开慢日志
现在很多人直接用Docker跑MySQL,开慢日志的方式稍微有点不一样。
如果你的MySQL容器已经跑起来了,最快的验证方式是在容器内执行SQL:
bash复制docker exec -it mysql8 mysql -uroot -p
sql复制SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL slow_query_log_file = '/var/lib/mysql/slow.log';
这种方式的缺点是:容器一旦重启,所有全局设置都会丢失。因为SET GLOBAL只是改了内存里的值,没有写进配置文件。
要永久生效,推荐用挂载配置的方式。先把宿主机上的my.cnf写好,然后启动容器时挂载进去:
bash复制docker run -d \
--name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=yourpassword \
-v /etc/mysql/conf.d/slow.cnf:/etc/mysql/conf.d/slow.cnf \
-v /opt/mysql-data:/var/lib/mysql \
mysql:8.0
注意挂载到/etc/mysql/conf.d/目录下,Docker官方镜像会自动读取这个目录下的.cnf文件。slow.cnf内容就是上一节那几行[mysqld]配置。
还有一个细节:如果MySQL的datadir是挂载的宿主机目录,慢日志文件默认也会写在datadir里。你可以在宿主机上直接tail这个文件:
bash复制tail -f /opt/mysql-data/slow.log
因为Docker容器内路径是/var/lib/mysql/slow.log,宿主机上就是/opt/mysql-data/slow.log。
3.3 验证慢日志是否真正生效
配置完之后,光看参数是ON还不够,得实际造一条慢SQL验证。
先执行一条延时查询,MySQL里可以用SLEEP函数:
sql复制SELECT SLEEP(2);
如果你设置的long_query_time是1秒,这条SLEEP(2)执行完就该进慢日志。去看日志文件:
bash复制tail -50 /var/log/mysql/slow.log
能看到类似这样的记录:
text复制# Query_time: 2.000350 Lock_time: 0.000000 Rows_sent: 1 Rows_examined: 0
SET timestamp=1710000000;
SELECT SLEEP(2);
看到Query_time: 2.000350,说明慢日志真的在工作。
注意一句:不要在业务高峰期在线上库里执行SLEEP测试,即使只是一条SLEEP(2),也会占用一个连接资源。我一般是在测试环境或者凌晨低峰期做。
3.4 将慢日志输出到表:log_output=TABLE的用法
除了写文件,MySQL还允许把慢日志写进mysql.slow_log表。设置方式:
sql复制SET GLOBAL log_output = 'TABLE';
此时慢日志会写入mysql.slow_log,你可以用SQL查询:
sql复制SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10\G
表模式的优势是可以直接用SQL聚合分析,比如按db字段分组看哪个库慢SQL最多:
sql复制SELECT db, COUNT(*) AS cnt, ROUND(AVG(query_time), 2) AS avg_query_time
FROM mysql.slow_log
GROUP BY db
ORDER BY cnt DESC;
但表模式有个非常坑的副作用:写慢日志本身会成为一条INSERT语句,而且这条INSERT还可能触发额外的磁盘写入,导致数据库负载反而升高。所以生产环境我建议优先用FILE,表模式适合在测试环境临时用。后面第六部分我再展开讲它的坑。
4. 读懂慢日志:字段解析与 mysqldumpslow/pt-query-digest 实战
4.1 一条典型慢日志逐字段拆解
拿到慢日志文件后,第一件事是能看懂一条记录。我随便摘一条典型的:
text复制# Time: 2025-01-15T14:23:45.123456Z
# User@Host: app_user[app_user] @ [192.168.1.10] Id: 12345
# Query_time: 3.876543 Lock_time: 0.321234 Rows_sent: 10 Rows_examined: 8654321
SET timestamp=1736951025;
SELECT a.id, b.name, a.order_no
FROM orders a
LEFT JOIN users b ON a.user_id = b.id
WHERE a.status = 1
ORDER BY a.create_time DESC
LIMIT 10;
逐行解释。
# Time行是SQL执行结束的时间点。8.0默认带时区信息,5.7可能是本地时间,这个看log_timestamps参数。
# User@Host行是执行这条SQL的账号和客户端IP。这个信息在定位“是哪个业务方产生了慢SQL”时非常有用。
Query_time是整条SQL的执行总耗时,也是判断慢不慢的核心指标。Lock_time是InnoDB层等待行锁、表锁、元数据锁等锁的耗时。注意,这不是“加锁操作本身”的耗时,而是“等待获取锁”的耗时。Rows_sent是实际返回给客户端的行数,Rows_examined是存储引擎扫描了多少行。
这里有个重要公式:Rows_examined远大于Rows_sent,几乎可以断定这条SQL有性能问题。比如上面这条,扫描了865万行,只返回10行,典型的没走索引或者索引选择失败。
SET timestamp=...是SQL开始执行的时间戳,下一行紧跟的才是真正的SQL语句。
看慢日志时,我习惯先看Rows_examined和Lock_time这两个字段。Rows_examined大,说明SQL本身扫描量大,优先看索引;Lock_time占比高,说明有锁等待,优先看并发事务。
4.2 mysqldumpslow:官方自带的轻量聚合工具
慢日志文件如果只有几十条,肉眼翻没问题。但如果每小时生成上千条,就得聚合分析了。MySQL自带的mysqldumpslow就是干这个的。
基本用法:
bash复制mysqldumpslow -s c -t 10 /var/log/mysql/slow.log
-s c表示按“出现次数”排序,-t 10表示只显示前10条。
其他常用排序方式:
-s t:按查询时间总和排序-s l:按锁等待时间排序-s r:按返回行数排序
还可以用-g过滤关键字:
bash复制mysqldumpslow -s c -g "orders" -t 20 /var/log/mysql/slow.log
这条命令会找出所有SQL文本里包含“orders”的慢SQL,并按出现次数排序。
mysqldumpslow有个特点:它会自动把SQL里的数字和字符串变量替换成N和S,也就是说不同参数的同类SQL会被聚合成同一条。比如SELECT * FROM orders WHERE id = 100和SELECT * FROM orders WHERE id = 200,会被聚合成SELECT * FROM orders WHERE id = N。这个特性对聚合分析同类慢SQL非常有用。
但在容器里跑mysqldumpslow有个注意点:官方MySQL镜像里不一定自带这个工具,有的精简镜像没有。你可以用docker exec进容器试一下,没有就得在宿主机装MySQL客户端工具,或者用下面的pt-query-digest代替。
4.3 pt-query-digest:进阶分析的正确打开方式
pt-query-digest是Percona Toolkit里的核心工具,分析慢日志比mysqldumpslow强大得多。
安装方法,CentOS系:
bash复制yum install -y percona-toolkit
Ubuntu系:
bash复制apt-get install -y percona-toolkit
基本用法:
bash复制pt-query-digest /var/log/mysql/slow.log
输出结果分三部分:总体摘要、按Query_time排序的前N条SQL、每条SQL的详细统计。
总体摘要里最关键的是Profile部分,它会列出消耗时间最多的SQL类型。举个实际例子:
text复制# Profile
# Rank Query ID Response time Calls R/Call V/M Item
# ==== ================== ============== ===== ======= ===== ====
# 1 0xA1B2C3D4E5F60708 12586.5092 58.6% 832 15.1275 0.01 SELECT public.orders
# 2 0x1122334455667788 4521.2345 21.0% 156 28.9823 0.05 SELECT public.users
Rank 1这条SELECT orders表,总耗时占了全部慢SQL的58.6%,平均一次15秒,一共执行了832次。这种数据拿给业务方看,完全不需要争论,问题在哪一目了然。
pt-query-digest还支持按时间范围分析、导出特定条件的SQL等,比如:
bash复制pt-query-digest --since "2025-01-15 12:00:00" --until "2025-01-15 14:00:00" /var/log/mysql/slow.log
只看某段时间内的慢SQL,在定位“为什么这两小时接口超时”时特别有用。
4.4 分析慢日志时的几个实用习惯
根据我个人的实践,整理几个分析习惯:
- 慢日志文件按天切割,分析时只拉当天的,不然文件太大,工具处理也慢。
- 先用pt-query-digest看总体Profile,找到Top 5的SQL,再回到慢日志文件里看这5条的详细SQL文本。
- 值得优化的SQL标准:总耗时占比高、出现次数多、单次扫描行数巨大。三条占两条,就该动手。
- 别忽略Rows_examined小但频繁出现的SQL,这类往往是指数选择错误,一次只扫几千行,但每秒调用上百次,累积起来也很要命。
5. 慢日志只是起点:三个典型场景的优化复盘
5.1 场景一:分页查询越来越慢,索引和覆盖索引怎么救
慢日志里最常见的一类问题就是深分页。SELECT * FROM orders ORDER BY create_time DESC LIMIT 100000, 20这种SQL,越往后翻越慢,慢日志里大量出现。
原因看执行过程就明白了:MySQL要先扫描前100020行,然后丢弃前100000行,只返回最后20行。扫描的行数(Rows_examined)会随着页码增大而线性增长,查询自然越来越慢。
解决思路有三个,按复杂度从低到高排。
第一,改成游标分页,也叫keyset pagination。如果业务允许,不要用LIMIT加偏移量,改成基于上次查询的最后一条记录id,比如:
sql复制SELECT * FROM orders
WHERE create_time < '2025-01-15 00:00:00'
ORDER BY create_time DESC
LIMIT 20;
这样每次查询MySQL都能直接走索引定位到起点,扫描的行数大约就是20行,不会越翻越慢。
第二,如果必须保留LIMIT分页,用延迟关联优化:
sql复制SELECT a.*
FROM orders a
INNER JOIN (
SELECT id
FROM orders
ORDER BY create_time DESC
LIMIT 100000, 20
) b ON a.id = b.id
ORDER BY a.create_time DESC;
先把要返回的主键id查出来,再通过主键回表查询完整数据。子查询里只查id,能走覆盖索引,回表次数限制在20次,避免扫描上万行再回表上万次。
第三,保证排序字段有合适的索引。上面的SQL如果ORDER BY create_time没有索引,MySQL会先建临时文件排序,扫描行数直接翻倍。在create_time上建单列索引,或者建(create_time)联合索引,都能让ORDER BY直接走索引有序性。
第四,用Redis做热点分页缓存。这个对应热词里的“分页查询慢怎么用redis优化”。但要说句实在话:分页缓存的适用场景有限,只适合数据基本不变、浏览量大、可以容忍短时间数据延迟的业务。比如排行榜、公告列表。
实现上,最简单的是把前N页的查询结果序列化后存Redis,设置5到30秒过期:
python复制def get_page_orders(page, size):
cache_key = f"orders:page:{page}:{size}"
data = redis.get(cache_key)
if data:
return json.loads(data)
orders = db.query("SELECT ... ORDER BY create_time LIMIT %s, %s", page, size)
redis.setex(cache_key, 30, json.dumps(orders))
return orders
注意几点:缓存key必须包含分页参数,否则不同页串数据;过期时间不要设太长,否则用户翻页时看到的是旧数据;冷门页不要缓存,否则Redis里全是垃圾数据。更稳妥的做法是只缓存第一页,后面的页走数据库。
5.2 场景二:慢日志里的Lock_time异常,死锁和阻塞排查
慢日志里如果出现很多Lock_time很大的记录,但Query_time本身不算离谱,这时候别急着去优化SQL,先处理锁问题。
我处理过一个典型case。业务反馈每天10点左右系统卡顿,慢日志里有一批UPDATE语句,单看执行很简单:
sql复制UPDATE inventory SET stock = stock - 1 WHERE product_id = 100;
Query_time大概2秒,Lock_time占了1.8秒。SQL本身没问题,product_id上有索引,扫描行数为1。问题在并发。
10点是活动秒杀开始时间,大量请求同时更新同一个商品库存,形成行锁竞争。每个事务都想拿同一行的X锁,后到的只能等前面提交。
排查步骤记录一下。
先确认当前是否有长时间运行的事务:
sql复制SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id,
trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;
重点看trx_started很早但trx_state是RUNNING的事务,这种多半是没提交的“僵尸事务”。
再查锁等待关系:
sql复制SELECT
r.trx_id AS waiting_trx_id,
r.trx_mysql_thread_id AS waiting_thread,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query
FROM performance_schema.data_lock_waits w
JOIN information_schema.innodb_trx r ON w.REQUESTING_ENGINE_TRANSACTION_ID = r.trx_id
JOIN information_schema.innodb_trx b ON w.BLOCKING_ENGINE_TRANSACTION_ID = b.trx_id;
最后用:
sql复制SHOW ENGINE INNODB STATUS\G
看LATEST DETECTED DEADLOCK段落,确认死锁发生的具体SQL。
优化方案要看业务。秒杀场景下的库存扣减,常见做法:
- 把事务尽量做短,先扣库存再异步通知,不要在事务里插入远程调用。
- 减少锁粒度,比如把单条库存记录拆分成多条库存桶,先随机选桶再扣。
- 对确实需要串行的热点库存,用Redis原子扣减先挡一层,流量削平后再异步落库。
慢日志在这里的作用是“报警器”,它告诉你系统里锁等待已经很严重了,但根因往往要结合业务逻辑看。
5.3 场景三:一条SQL从慢到快的完整过程
最后给一个纯SQL优化的完整前后对比。慢日志里经常出现这类语句:
sql复制SELECT order_no, user_id, amount, status, create_time
FROM orders
WHERE status = 1
ORDER BY create_time DESC
LIMIT 20;
orders表有500万行,status字段的值只有0、1、2三种,create_time上有索引。
执行计划看起来可能不差,但实际经常慢。为什么?因为status = 1查询出的记录可能占全表的30%,MySQL优化器会认为“用索引没意义,直接全表扫描更快”,于是放弃create_time索引,走全表扫描,再用filesort排序。Rows_examined是500万,Query_time自然就上去了。
优化思路:
第一,组合索引。建一个(status, create_time)联合索引:
sql复制ALTER TABLE orders ADD INDEX idx_status_create_time (status, create_time);
这样MySQL可以先用status过滤,再直接按create_time顺序取数据,排序也能走索引,避免filesort。
第二,如果只查那4个字段,进一步用覆盖索引:
sql复制ALTER TABLE orders ADD INDEX idx_status_create_time_cover (status, create_time, order_no, user_id, amount);
让查询的所有字段都在索引里,这样连回表都省了,InnoDB直接从索引叶子节点拿数据。
加了覆盖索引后,Rows_examined从500万直接降到几十,Query_time从2.3秒降到几毫秒。这种效果在慢日志上看最直观:优化前的慢日志里这条SQL每天都出现,优化后再也不出现了。
需要注意一点:冗余联合索引会拖慢写入性能,尤其是订单表这种写操作频繁的表,加索引前要评估写入量。一般我会建议先加(status, create_time)二列索引,不够再加覆盖列。索引不是越多越好,能解决问题就好。
6. 慢查询日志的真坑:膨胀轮转、TABLE模式与误判预防
6.1 日志文件无限增长:轮转策略怎么设计
慢查询日志最大的坑就是日志文件无限膨胀。线上业务如果慢SQL多,一天能生成几个G的日志,磁盘满了之后,数据库会出各种幺蛾子。
Linux下最简单的方式是用logrotate按天切割。以CentOS为例,在/etc/logrotate.d/下新建一个mysql文件:
text复制/var/log/mysql/slow.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 640 mysql mysql
sharedscripts
postrotate
/usr/bin/mysqladmin -uroot -p密码 flush-logs
endscript
}
daily表示每天切割一次,rotate 14保留14份,compress压缩旧日志。
关键点在于postrotate里的mysqladmin flush-logs。这一步是让MySQL关闭当前日志文件并创建新文件。如果没有flush-logs,慢日志会继续写进已经被改名的旧文件,切割就白切了。
如果你用的是Docker容器,logrotate在宿主机上跑会比较绕。我现在的做法是:把慢日志写到宿主机挂载目录,然后用宿主机crontab每天凌晨执行一次切割:
bash复制0 0 * * * docker exec mysql8 bash -c "mysqladmin -uroot -p密码 flush-logs"
然后在宿主机上对/opt/mysql-data/slow.log做mv改名,再执行上面的flush-logs。顺序不能反,先mv再flush-logs。
6.2 log_output=TABLE的教训:mysql.slow_log表也会膨胀
前面提到log_output=TABLE把慢日志写入mysql.slow_log表。看起来方便,实际上问题很多。
第一个问题是mysql.slow_log表一样会膨胀。慢SQL多的时候,这张表能涨到几十个G,而且它本身也是InnoDB表,查询这张表反而会产生新的慢查询,形成恶性循环。
第二个问题是清理方式有讲究。直接DELETE FROM mysql.slow_log在小数据量时没问题,但表很大的时候,DELETE会产生大量undo和binlog,导致主从延迟,甚至拖垮数据库。正确做法是清空表:
sql复制TRUNCATE TABLE mysql.slow_log;
但TRUNCATE会重置自增ID,而且需要DROP权限,普通账号不一定有。
第三个问题是慢日志表如果涨得太快,会影响备份。mysqldump备份时默认会备份mysql库,一张几十个G的slow_log表会让备份时间变长不少。所以用TABLE模式一定要配上定时清理,还要在备份脚本里排除这个表。
我的建议是:生产环境老老实实用FILE,把日志文件交给logrotate管理。TABLE模式只适合测试环境快速分析。
6.3 误判预防:为什么日志里会出现一堆“不算慢”的SQL
开了log_queries_not_using_indexes之后,慢日志里会出现大量“没走索引但执行很快”的SQL,比如:
sql复制SELECT name FROM regions WHERE code = '110000';
regions表一共几千行,扫描全表也就几毫秒,但因为没走索引,被记录进来了。这种情况不是问题SQL,是过度记录。
避免误判的关键参数是min_examined_row_limit。这个参数的意思是:扫描行数低于这个值的SQL,即使没走索引,也不记录。
我的配置建议是:
ini复制slow_query_log = 1
long_query_time = 1
log_queries_not_using_indexes = 1
min_examined_row_limit = 1000
log_throttle_queries_not_using_indexes = 100
长话短说:
- min_examined_row_limit=1000,过滤掉扫描小表的查询,避免无意义记录。
- log_throttle_queries_not_using_indexes=100,限制每分钟最多记录100条未走索引的SQL,防止某一次bug导致日志爆炸。
这几个参数配合起来,慢日志的“信噪比”会高很多。
6.4 长事务与慢日志的关系
还有一个容易误导人的地方:慢日志里没有记录某条SQL,不代表数据库没有问题。
长事务就是一个典型。一个事务里执行了很多条SQL,每条都很快(小于long_query_time),但整个事务从开始到提交持续了几分钟。这种情况下,不会产生慢日志,但事务本身占用了大量连接、锁资源,甚至导致其他SQL阻塞而变慢。
所以排查阻塞问题时,不能只依赖慢日志。我会同时查information_schema.innodb_trx,找trx_started时间过长的空闲事务:
sql复制SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec
FROM information_schema.innodb_trx
WHERE trx_state = 'RUNNING'
ORDER BY trx_started
LIMIT 10;
看到duration_sec超过300秒的事务,就要警惕,它们往往是锁等待的源头。慢日志负责发现问题SQL,事务表负责找出持有锁的“元凶”,两者配合才完整。
6.5 我的实际经验总结
最后啰嗦几句我这几年的习惯。
新接手的MySQL实例,我第一件事一定是开慢查询日志,long_query_time设1秒,log_queries_not_using_indexes先不开。运行两三天后再开着两个参数去分析,同时用logrotate把日志切割配好。
每次优化完一条慢SQL,不是看执行计划变快了就完事,而是要回到慢日志里观察它后续是否还出现。我会在分析当天做一个全量快照,一周后再跑一次pt-query-digest对比,用数据说话:慢SQL数量下降了多少,总耗时下降了百分之几。这个方法比凭感觉判断靠谱得多。
慢日志不是银弹,它解决的是“定位哪些SQL有问题”这一步。真正难的是后面根据业务场景做最合适的优化,这可能涉及加索引、改SQL、调参、改表结构,甚至引入缓存。但如果没有慢日志这一步,后面的优化都是盲人摸象。把这一步做扎实,你的MySQL性能排查就已经赢了一半。
