如果你的线上MySQL实例最近响应变慢,但又说不出慢在哪个具体环节,手头还没有可靠的排查依据,那我建议你先别急着调参数或加缓存,第一步应该打开慢查询日志看看。这是所有MySQL性能排查里成本最低、信息量最大的一步,没有之一。
这篇文章我会从慢查询日志的完整配置讲起,再到日志内容的逐行解读、常见误区和坑,最后用一个实际案例把"从发现慢SQL到定位根因"的整个链路走一遍。内容适合刚接触MySQL优化的人,也适合已经用过慢查询日志但想系统梳理一遍的开发者。我尽量说人话,把底层逻辑讲清楚,保证你看完能直接上手。
1. 慢查询日志到底是什么:它记录什么、不记录什么
慢查询日志是MySQL官方提供的一种运行日志,专门用来记录执行时间超过指定阈值的SQL语句。它的核心作用只有一个:帮你回答"我的数据库时间到底花在哪条SQL上了"这个问题。
默认情况下,MySQL的慢查询日志是关闭的。很多从开发转过来的同事第一次排查性能问题时,习惯性地执行SHOW VARIABLES LIKE 'slow_query_log',发现结果是OFF,这时候就需要先开启它。这里有个关键点值得强调:不只是执行时间长的SELECT会被记录,UPDATE、DELETE、INSERT这些写操作同样在慢查询日志的监控范围内。我实际遇到过不少案例,系统的写放大问题就是靠慢查询日志里那些执行了十几秒的UPDATE语句暴露出来的。
要理解慢查询日志,先要搞清楚MySQL判断一条SQL"慢不慢"的标准是什么。它主要看三个变量:
- long_query_time:执行时间超过这个阈值(单位秒,默认10秒)的SQL才会被记录,而且这个时间是实际执行时间,不包括等待锁的时间。
- log_queries_not_using_indexes:开启后会记录所有没有走索引的查询,这个选项非常有用,后面我会专门展开讲。
- min_examined_row_limit:只有当SQL扫描行数超过这个值的才会被记录,通常配合上面那项使用,避免把扫描行数很少但没走索引的小查询也记录下来。
这三个变量组合起来,决定了慢查询日志的"灵敏度"。默认的10秒阈值对大多数业务场景来说太宽松了,线上一个INSERT事务跑个3秒可能已经是灾难级延迟,但用默认配置它根本不会被记录。所以我一直建议,生产环境至少把long_query_time调成1秒,业务高峰期能接受的极限延迟是多少,阈值就设成多少。
还需要澄清一个常见误解:慢查询日志记录的不仅仅是"慢"的SQL,也包括了执行计划不合理的SQL。比如一张表只有几千行数据,一条全表扫描的查询可能只跑了200毫秒,从执行时间看它不算慢。但如果你开启了log_queries_not_using_indexes,它同样会被记进日志里。这类SQL单独看不致命,问题是当表数据量涨到几百万行时,同样的执行计划很可能会从200毫秒恶化到几秒。慢查询日志在这个层面其实是执行计划质量的晴雨表。
一句话总结:慢查询日志的价值不在于"事后追责",而在于提供一条从现象到根因的侦查线索。它不告诉你SQL为什么慢,但告诉你"该往哪查"。
2. 一步步开启慢查询日志:参数说明与实操命令
开启慢查询日志的方式有两种:一种是临时修改运行时变量,服务器重启后失效;另一种是写进配置文件,永久生效。我建议在排查问题阶段用前者,确认配置参数合适后再固化到配置文件里。
先看临时开启的方式。MySQL 5.7及8.0版本默认都支持以下命令:
mysql> SET GLOBAL slow_query_log = 'ON'; mysql> SET GLOBAL long_query_time = 1; mysql> SET GLOBAL log_queries_not_using_indexes = 'ON';这里有一个我在实际运维中经常看到的坑:修改long_query_time后,当前已经打开的会话里这个值不会自动更新,只有新建立的连接才会生效。如果你改了参数之后发现日志里还是只有超过10秒的SQL,先别怀疑参数没生效,检查一下是不是用了老的连接在跑查询。
mysql> SHOW VARIABLES LIKE 'long_query_time'; mysql> SHOW VARIABLES LIKE 'slow_query_log_file';查看慢查询日志的存放位置和当前状态,建议开启后第一时间确认日志文件路径,避免之后找不到日志在哪。默认路径一般在数据目录下,文件名通常是主机名-slow.log这种格式。如果你用的是Docker部署的MySQL,特别容易踩这个坑——容器里的日志目录如果没有做volume映射,容器一删日志就没了,排查到一半才发现证据丢了,非常尴尬。
再把参数固化到配置文件里。MySQL的配置文件在Linux上通常是/etc/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 min_examined_row_limit = 100关于log_queries_not_using_indexes这个选项,我想多说两句。生产环境中如果直接把它设成ON,并且不设置min_examined_row_limit,很可能会被日志刷屏。因为很多小表查询都不走索引,这些查询本身执行得很快,大量记录会迅速填满磁盘。正确做法是同时设置min_examined_row_limit,让它只记录扫描行数超过一定数量的无索引查询。比如min_examined_row_limit=1000,就表示一条查询至少要扫1000行才会被记录。
配置文件修改后需要重启MySQL服务才能生效,这是和临时方式最大的区别。如果你用的是云数据库RDS,一般都有参数组功能,直接在控制台修改对应参数并应用即可,不用自己重启实例。
还有一种输出方式值得了解:MySQL可以把慢查询日志写到表里,也就是mysql.slow_log表,用log_output参数控制。
mysql> SET GLOBAL log_output = 'TABLE';写入表的好处是可以用SQL直接查,比如按执行时间排序找出最慢的几条SQL,比读文件方便得多。但缺点是表写入本身有开销,高并发场景下不建议长期使用。我自己的习惯是:日常排查用文件日志,偶尔需要快速统计汇总才临时切到TABLE模式。MySQL 8.0里慢日志表用的是CSV存储引擎,查询统计时效率不高,可以先转换成InnoDB再分析:
ALTER TABLE mysql.slow_log ENGINE = InnoDB;不过这里要提醒一下,mysql.slow_log表本身也可能被记录到慢查询日志里,形成一种递归记录的现象。实际使用中需要留意,避免分析日志时混入噪音数据。
3. 慢查询日志内容逐行拆解:从一条真实日志学起
打开慢查询日志文件,里面不是表格也不是JSON,而是一段段连续的多行文本。先看一个我在实际业务中抓到的真实例子:
# Time: 2024-06-15T10:32:18.123456Z # User@Host: app_user[app_user] @ [192.168.1.101] Id: 891023 # Query_time: 3.812345 Lock_time: 0.000182 Rows_sent: 1 Rows_examined: 537871 SET timestamp=1718447538; SELECT id, order_no, amount, status FROM orders WHERE buyer_id = 123456 AND status = 'paid' ORDER BY create_time DESC LIMIT 20;逐行来看这些信息:
- Time:SQL执行的时间戳,注意这里默认是UTC时间,如果你的服务器时区是东八区,需要在分析时把时间加上8小时,否则你可能会发现"日志和业务高峰期对不上"。
- User@Host:执行SQL的用户名和客户端IP,多业务共库的环境里,这个字段能快速定位是哪个应用发起的请求。
- Id:连接线程ID,可以配合SHOW PROCESSLIST或者performance_schema去关联当时的会话状态。
- Query_time:这是最核心的字段,表示SQL实际执行耗时。3.812345秒,这个值超过阈值所以被抓进来了。
- Lock_time:等待锁的时间。这个时间容易被误读,很多人以为它指的是行锁等待,其实它统计的是MySQL层等待表锁、元数据锁的时间,InnoDB的行锁等待时间主要包含在Query_time里。
- Rows_sent:最终返回给客户端的行数。这里只有1行,但实际扫描了53万行,筛选效率极低。
- Rows_examined:执行过程中扫描的行数。这个数字直接反映了SQL的"体力活"有多大。理想情况下Rows_examined和Rows_sent应该接近,如果两者差距悬殊,说明索引设计或者SQL写法有明显问题。
- SET timestamp:执行快照时的Unix时间戳,用于还原SQL执行时的上下文环境。
- SQL文本:真正被执行的那条语句,多行SQL会完整保留换行格式。
分析这么多字段,最该抓住的其实是三个数字:Query_time、Rows_sent、Rows_examined。5秒能跑完的SQL不可怕,可怕的是每条都要扫50万行才返回几行。优化思路就是在扩大扫描和缩小扫描之间做文章——通过索引、改写SQL或者调整业务逻辑,把Rows_examined降下来,Query_time自然就降下来了。
除了这些字段,慢查询日志还有一种格式上的变化。MySQL 5.7开始支持将慢查询日志记录到系统表时使用更精细的时间精度,8.0版本还能看到事务提交信息。不过万变不离其宗,核心字段还是上面这些,分析思路不需要变。
4. 用mysqldumpslow和pt-query-digest分析日志:常用工具的真实对比
拿到慢查询日志文件后别急着逐条读,慢日志是流水账,如果线上SQL量大,文件能达到几百MB甚至上GB。这时候需要工具帮忙做聚合统计。MySQL自带一个mysqldumpslow工具,Percona Toolkit里有一款更专业的pt-query-digest,我两个都用过,给你分享一下真实的使用心得。
先看mysqldumpslow,它是MySQL安装包自带的,不需要额外安装。常用姿势是:
# 按平均耗时排序查看前10条最慢的SQL(归一化后的形式) mysqldumpslow -s at -t 10 /var/lib/mysql/mysql-slow.log输出结果会将SQL文本中的数字参数归一化成N,比如WHERE id = 123456会被显示为WHERE id = N,这样功能相同的SQL就能被聚合到一类里。这个设计非常实用,否则成千上万条只差参数值的SQL会让统计完全失去意义。
mysqldumpslow支持按多种维度排序:c表示计数,t表示总耗时,at表示平均耗时,l表示锁等待时间,r表示返回行数。我建建议现场排查时先看at(平均耗时),再看c(出现次数),两个维度交叉起来,能找到"频率高且耗时高"的高优先劣化对象。
再来看pt-query-digest,这是Percona Toolkit的核心工具,功能比mysqldumpslow强很多,但需要单独安装。使用方式:
# 解析慢查询日志,输出到报表文件 pt-query-digest /var/lib/mysql/mysql-slow.log > slow_report.txt生成的报告结构大概是这样的层次:
- 第一部分是整体报告,列出总查询数、耗时分布、各个时间段的活跃情况。
- 第二部分按查询的"指纹"(fingerprint)分组排名,每组会展示典型SQL、总耗时、平均耗时、出现次数、Rows_examined和Rows_sent的百分位数。
pt-query-digest比mysqldumpslow强的地方在于它能识别参数化后的查询指纹,聚合更准确,还能关联查询出现的时间分布。如果你是第一次到客户现场排查MySQL性能问题,我建议优先用pt-query-digest,信息密度高很多,也能省去手工去换算的时间。
工具对比总结如下:
| 对比项 | mysqldumpslow | pt-query-digest |
|---|---|---|
| 安装复杂 | 低,随MySQL自带 | 中,需安装Percona Toolkit |
| 聚合准确度 | 中,基本的参数归一化 | 高,指纹聚合,识别相似查询 |
| 时间分布分析 | 不支持 | 支持,能看到一天内各时段热点 |
| 输出详细度 | 简单排行 | 报告完整,含执行计划建议 |
| 适合场景 | 快速粗筛 | 深度定位、性能审计 |
实际工作里我会先用mysqldumpslow快速看一眼全局,如果发现需要深挖的SQL再切pt-query-digest生成报告。现场解决问题时工具次数不重要,能最快定位问题最重要。
5. 一个从慢查询日志到索引优化的完整排查案例
讲完工具,用一个贴近真实业务的案例把排查链路串起来。假设你负责订单系统,最近陆续收到业务方反馈"订单查询变慢",你打开了慢查询日志,发现有一类SQL频繁出现:
# Query_time: 2.867419 Lock_time: 0.000108 Rows_sent: 20 Rows_examined: 489231 SELECT id, order_no, buyer_id, status, amount, create_time FROM orders WHERE status = 'paid' ORDER BY create_time DESC LIMIT 20;第一步先分析这个SQL的特征。Rows_examined达到了48万多,Rows_sent才20,扫了大量数据但只返回20行,典型的"大范围扫描+排序+取头部"模式。问题出在哪里,接下来用EXPLAIN看一下执行计划:
mysql> EXPLAIN SELECT id, order_no, buyer_id, status, amount, create_time FROM orders WHERE status = 'paid' ORDER BY create_time DESC LIMIT 20;执行结果会显示type是ALL或ref,可能走了一个选择性很差的索引,然后Extra列出现Using filesort。这说明MySQL先按status筛选出一大批记录(有status索引的话),再在内存或磁盘里排序,最后取前20行返回。
问题核心很清楚:status字段的选择性太低了。订单表中绝大部分订单最终都变成paid状态,用status做索引筛选出的结果集几乎等同于全表。MySQL需要把接近50万行都拉进排序缓冲区,做一次完整的排序操作,再丢弃后面的记录。它真正想要的是直接按create_time倒序扫描,遇到status='paid'的就返回,这样只要扫前几条就能拿到结果。
优化方案先想到的是建立(status, create_time)复合索引。这个索引可以有效过滤status,并保持create_time有序:
ALTER TABLE orders ADD INDEX idx_status_create_time (status, create_time);建立索引后,EXPLAIN的类型会变成ref,Extra不再出现Using filesort。因为查询只需沿着索引找到第一条status='paid'的记录,连续取20条即可,扫描行数会从48万骤降到几十行。
不过我还想多说一种反直觉的优化思路:把顺序反过来建索引,也就是(create_time, status)。这种情况下,查询直接按create_time倒序走索引,每遇到一条记录就检查status是否为'paid',如果是就直接返回。因为订单更新后通常会尽快支付,近期订单里paid占比很高,往往扫几条就能凑齐20条,扫描行数可能比(status, create_time)更少。这两种方案各有适用场景,需要结合业务数据分布来决定。
优化完再看实际效果。执行同样的查询,耗时从2.8秒降到30毫秒,Rows_examined从48万降到40行左右。这个案例说明一个朴素的道理:慢查询日志里的每个数字都不是随便填的,Rows_examined和Rows_sent的差距就是你在数据空间里白白付出的"体力活"。
同类的SQL还有几种变形,比如按时间范围查最近一小时未发货的订单、按状态加金额区间做筛选、分页深翻页时offset过大等等。抓到一个慢查询,不要只修这一条,而是抽象出模式,去排查同类写法。
6. 被忽略的问题:慢查询日志本身的副作用和运维经验
慢查询日志帮助我们揪出性能问题,但日志功能本身也会引入新的性能开销和运维负担。先说性能层面:日志文件本质上就是磁盘写入操作,当开启了log_queries_not_using_indexes后,如果业务中无索引查询数量很大,慢日志可能以每秒几百条的速度增长,持续写入会占用大量IO。这种IO问题在机械磁盘上尤其严重,在SSD上相对好一些,但也不是完全没有影响。
针对这个问题,有几点实操建议:
- 生产环境长期开启,但阈值要合理。long_query_time至少设成1秒或2秒,不建议设成0.1秒这种过低的阈值,否则日志量会非常庞大。
- 如果不是正在做专项排查,不建议同时开启log_queries_not_using_indexes和很小的min_examined_row_limit。建议min_examined_row_limit至少1000起步。
- 日志文件建议配置rotate策略。MySQL本身不提供自动轮转,你需要在系统层面配置logrotate,按天或按大小切割文件,保留最近7-30天即可。否则日志文件越滚越大,后续分析和磁盘空间都会出问题。
再分享一个我踩过两次的坑:慢查询日志和binlog不要放同一块磁盘。慢查询日志是持续写入的,binlog也是持续写入的,两个高写入量的文件放在同一个磁盘上,会导致IO争用。如果条件允许,把log_slow_query_log_file放到独立的磁盘或至少不同的目录挂载点上。
还有权限相关问题。多人在同一套环境上排查问题时,如果使用普通账号连接数据库,需要确认该账号是否有权限修改全局参数,特别是使用SET GLOBAL slow_query_log='ON'时。生产环境的账号通常不建议授予SUPER或SYSTEM_VARIABLES_ADMIN权限,可以考虑由DBA统一开启。
在处理慢查询日志的存储时,我建议对写入日志文件配置压缩或定期归档。分析完毕后,文件可以gzip压缩起来,节约空间,同时保留证据以备后续复盘。很多团队在性能治理结束后直接把日志文件删掉,等下次再需要分析时发现证据全没了,容易把问题排查变成无源之水。
7. 慢查询日志解决不了的问题:配合performance_schema和EXPLAIN形成排查闭环
熟练使用慢查询日志之后,你会发现它有一个边界:它告诉你"哪条SQL慢",但不告诉你"这条SQL为什么慢"。SQL慢的原因可以从几个层面分析,慢查询日志只能覆盖到最外层。
慢查询日志解决不了的场景大致有几类:
第一类是单条SQL执行时间正常(比如200毫秒),但每秒被调用几千次,数据库整体CPU被打满。慢查询日志里全是"不够慢"的记录,从日志入手很难看出热点。这时需要借助performance_schema的events_statements_summary_by_digest表,按调用次数排序找出"访问频率最高的SQL",从频率维度补足慢查询日志覆盖不到的盲区。
SELECT SCHEMA_NAME, DIGEST_TEXT, COUNT_STAR, SUM_TIMER_WAIT FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;第二类是SQL本身执行计划漂亮,索引也走了,但锁等待时间很长。慢查询日志里的Lock_time统计的是MySQL层锁,InnoDB行锁的等待时间包含在Query_time里,不做细分。要定位锁具体卡在哪个事务上,需要配合SHOW ENGINE INNODB STATUS里的LATEST DETECTED DEADLOCK或者sys.innodb_lock_waits视图来看。
第三类是复杂SQL的中间步骤。比如一条SQL里有子查询、多表JOIN、临时表操作,靠慢查询日志只能看到总耗时,看不到时间消耗在哪个具体步骤。这时建议在SQL前加EXPLAIN ANALYZE(MySQL 8.0.18+),它能输出每一步的耗时和扫描行数,比单纯EXPLAIN更接近真实执行情况。
EXPLAIN ANALYZE SELECT ... FROM orders o JOIN order_items oi ON o.id = oi.order_id WHERE o.buyer_id = 123456;不同类型的问题对应不同的诊断工具,我梳理一下我现场排查时的参考链路:
| 现象 | 首选工具 | 备选工具 |
|---|---|---|
| 偶发性慢SQL | 慢查询日志 | performance_schema的events_statements |
| CPU持续高,慢日志不明显 | events_statements_summary_by_digest按频率排序 | sys.statement_analysis |
| 锁等待导致慢查询 | SHOW ENGINE INNODB STATUS | sys.innodb_lock_waits |
| 单条SQL内部耗时分布 | EXPLAIN ANALYZE | optimizer_trace |
| 磁盘IO高但SQL正常 | 慢日志写入量评估 | 系统级iostat |
这套组合打法能覆盖我日常遇到的大部分性能排查场景。慢查询日志是入口,但不是终点。
8. 慢查询日志相关的几个高频面试问题
关于慢查询MySQL慢查询日志这个主题,面试和团队内部技术分享中也常被问到。整理几个高频问题,帮你同时巩固理解。
第一个问题:慢查询日志对数据库性能有多大影响?回答思路:慢查询日志本身需要额外的IO写入,如果阈值设置过低或者记录了过多无索引查询,会放大IO压力。但比日志本身更伤性能的是"慢查询对应的SQL",日志不会把数据库变慢,它只是暴露了变慢的根源。
第二个问题:long_query_time改成1秒后,当前连接不生效?原因在于系统变量需要新连接才会重新读取。这个细节很多人踩过,面试官问出来也是想看你是不是真的操作过。
第三个问题:慢查询日志里Rows_examined很大但Rows_sent很小,说明什么?说明SQL做了大量无效扫描,通常代表索引缺失、索引选择性差、或者SQL写法导致执行计划走了不合理的路径。这是典型的索引优化信号。
第四个问题:如何区分一个慢SQL是IO瓶颈还是CPU瓶颈?这就不能只看慢查询日志了,需要结合系统层面的top、iostat看CPU和IO消耗,再用EXPLAIN ANALYZE看执行计划内部耗时。慢日志告诉你"什么慢",系统监控告诉你"资源的瓶颈在哪"。
第五个问题:线上是否应该长期开启慢查询日志?我的答案是一般建议长期开启,但阈值要合理。大多数业务场景1秒这个阈值是比较合适的,既不会产出过多日志,又能捕获绝大多数性能劣化。至于log_queries_not_using_indexes,可以定期开启一段时间做索引质量检查,日常不建议长期开。
这些问题背后没有标准答案,核心考察的是对慢查询日志机制的理解深度和真实操作经验。
9. 我给新手的执行清单:从零开始建立慢查询监控
最后分享一份可以照着做的执行清单,帮你一步步把慢查询监控建起来,避免遗漏关键环节。
第一步:确认当前配置状态。执行SHOW VARIABLES LIKE 'slow_query_log',检查当前慢查询是否开启、日志路径在哪、阈值是多少。把这三项记下来。
第二步:开启慢查询并设置合理阈值。临时开启至少看当前效果,建议long_query_time从1秒开始。开启后确认新连接生效。
第三步:抓取至少一天的日志。不要开启半小时就开始分析,一天是基线周期,能覆盖大多数业务高峰。如果接入层有明显的波峰波谷,至少覆盖一个完整波峰。
第四步:用工具聚合分析。先mysqldumpslow粗筛,再用pt-query-digest做完整报告,重点看avg时长靠前、出现次数靠前的SQL,用Rows_examined和Rows_sent的差距找出索引优化对象。
第五步:对每条目标SQL做EXPLAIN和EXPLAIN ANALYZE,确认瓶颈具体在过滤、排序还是关联。不要跳过这一步直接建索引。
第六步:优化后重新抓日志,对比优化前后Query_time、Rows_examined的数值变化。如果优化有效,指标应当有数量级层面的改善。
第七步:把慢查询日志纳入自动化监控体系。定期分析日志,把TOP SQL变化趋势作为一个常态化指标跟踪,不要出了问题才想起来看。
这套流程我用了很多年,整理出来就是这么朴素直接。MySQL的排查工作从来没有银弹,慢查询日志是最扎实的起点。把它看透彻,再配合performance_schema和EXPLAIN ANALYSIS一层层往下挖,绝大多数性能问题都能找到清晰的优化路径。