Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL慢查询日志

MySQL慢查询日志详解(附详细案例)

作者:写后端的胖头鱼

慢查询日志是MySQL提供的一种核心性能优化工具,用于记录执行时间超过指定阈值的SQL语句,这篇文章主要介绍了MySQL慢查询日志的相关资料,文中通过代码介绍非常详细,需要的朋友可以参考下

概念

慢查询日志(slow query log)是 MySQL 记录执行时间超过指定阈值SQL 的日志,用来定位慢 SQL、性能瓶颈,是 SQL 优化最基础的工具。

一、慢 SQL

慢 SQL 不是语法错误的 SQL,是执行效率低下,消耗大量数据库资源(CPU、IO、锁),执行耗时较长的 SQL 语句。 简单说:跑的慢、耗资源,拖垮 MySQL 的 SQL,就叫慢 SQL。

慢查询日志只是用来捕获慢 SQL 的工具;慢 SQL 是对象,慢查询日志是记录工具,二者不要混淆。

判定标准

有两个核心判断维度,不能只看执行时间

  1. 执行耗时:超过 long_query_time 阈值(比如 1s),慢查询日志会捕获。
  2. 扫描行数 (Rows_examined):扫描大量数据,哪怕返回很少结果、耗时很短,依然属于潜在慢 SQL。

举例:一条 SQL 执行 0.4 秒,没达到 1s 阈值不会进慢日志,但扫描 20 万行。数据量增长后,未来会变成严重慢 SQL,这就是隐形慢 SQL,最容易被忽略。

慢 SQL 常见成因

1. 索引问题(占 80% 以上场景)

2. SQL 写法问题

3. 业务 & 数据层面

4. 数据库环境因素

慢 SQL 带来的危害

  1. CPU/IO 飙升,数据库整体响应变慢
  2. 锁等待堆积,连接数打满,应用报错
  3. 连锁雪崩:一条慢 SQL 占满资源,正常业务 SQL 也被拖慢
  4. 主从延迟:大 DML 慢 SQL 造成从库复制延迟

如何发现慢 SQL

  1. 慢查询日志 slow_query_log:事后记录已经执行完的慢 SQL
  2. show processlist / show full processlist:抓正在运行的慢 SQL
  3. performance_schema:线上持续监控 SQL 执行指标
  4. APM 工具(SkyWalking 等):应用侧上报慢 SQL
  5. explain:拿到 SQL 分析执行计划,看 type、key、rows、Extra

常见误区

❌慢 SQL 就是执行时间很长的 SQL

错,存在大量隐形慢 SQL:耗时短,但扫描行数巨大。

❌没出现在慢查询日志,就不是慢 SQL

阈值设置过高,隐形慢 SQL 不会落日志。

❌ explain 看到 type 不是 ALL 就没问题

range 也可能扫描几十万行,依然是慢 SQL。

❌测试环境跑很快,线上就是慢

测试库数据量小,看不出全表扫描问题;线上真实数据量才会暴露。

简单例子

-- 坏:name无索引,全表扫描百万行
select * from user where name = '张三';

-- 坏:大偏移分页,扫描前面10万行
select id,name from order limit 100000,10;

-- 坏:索引列上用函数,索引失效
select * from user where DATE(create_time)='2026‑08‑27';

慢 SQL 和慢查询日志的区别

  • 慢 SQL:低效 SQL 语句本身
  • 慢查询日志:MySQL 提供记录慢 SQL 的日志文件,是发现慢 SQL 的手段。

二、慢 SQL日志

1. 核心参数

参数说明
slow_query_log是否开启慢查询日志:ON开启,OFF关闭,默认 OFF
slow_query_log_file慢日志文件路径,例如 /var/lib/mysql/mysql-slow.log
long_query_time慢查询阈值,单位秒,SQL 执行时间超过该值就记录;默认 10 秒,可以设置小数如0.5代表 500ms
log_queries_not_using_indexes记录没有使用索引的 SQL,即使执行时间没达到 long_query_time,生产谨慎开启,大库会产生大量日志
log_throttle_queries_not_using_indexes限制每分钟记录无索引 SQL 的条数,防止日志爆盘,配合上面参数使用
log_output日志输出方式:FILE文件;TABLE存入 mysql.slow_log 表;FILE,TABLE两者同时

注意:long_query_time统计的是实际执行时间,不包含锁等待时间。SQL 命中慢查询条件才写入日志。

2. 开启慢查询

方式 1:临时开启(重启 MySQL 失效)

set global slow_query_log = ON;
set global long_query_time = 1; -- 超过1秒就记录
set global log_queries_not_using_indexes = OFF;

global 修改后,已建立的会话不生效,需要重新连接数据库。

方式 2:永久配置 my.cnf/my.ini

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/lib/mysql/mysql-slow.log
long_query_time = 1
log_queries_not_using_indexes = 0
log_output = FILE

⚠️:修改配置文件需要重启 MySQL 服务。

查看当前配置

show variables like '%slow_query%';
show variables like 'long_query_time';

3. 慢查询日志文件内容解读

示例日志片段:

# Time: 260827 10:20:30
# User@Host: root[root] @ localhost []  Id:    12
# Query_time: 2.345678  Lock_time: 0.000123 Rows_sent: 100  Rows_examined: 100000
SET timestamp=1787826030;
select * from big_table where name='xxx';

字段释义:

  1. Query_time:SQL 实际执行耗时 2.34s(核心指标)
  2. Lock_time:等待锁的时间
  3. Rows_sent:返回给客户端行数
  4. Rows_examined:扫描的行数,这个值很大代表扫描大量数据,是优化重点
  5. SET timestamp:SQL 执行时间戳

重点看:Query_time 看慢;Rows_examined看扫描量,很多 SQL 耗时不长,但扫描百万行,未来数据量上涨就会变卡。

4. 分析工具:mysqldumpslow

MySQL 自带工具,解析慢日志,汇总统计,不用肉眼看庞大日志文件。

常用命令

# 查看帮助
mysqldumpslow --help

# 按照查询时间排序,取前10条慢SQL
mysqldumpslow -s t -t 10 /var/lib/mysql/mysql-slow.log

# 按照扫描行数排序(看扫描量大的SQL)
mysqldumpslow -s r -t 10 mysql-slow.log

# 匹配包含select的语句
mysqldumpslow -s t -g "select" mysql-slow.log

参数说明:

mysqldumpslow 会把值替换成N、S做聚合,相同结构不同参数的 SQL 合并统计,方便看高频慢 SQL。

第三方工具

5. 生产最佳实践

  1. 阈值设置:线上建议 long_query_time=1(1 秒),业务敏感可以调到 0.5 秒;不要设置 0,会记录全部 SQL,IO 压力巨大。
  2. log_queries_not_using_indexes:生产环境不建议常开,压测、排查阶段临时打开即可。
  3. 日志文件要做轮转切割,防止日志占满磁盘,配合 logrotate。
  4. 慢查询日志只做发现问题,拿到 SQL 后一定要用 explain 分析执行计划,定位索引、全表扫描问题。
  5. 不要长期开启输出到 TABLE 模式,mysql.slow_log表数据量大也会影响性能。

6. 常见误区

  1. ❌:long_query_time=0 全部记录,线上禁止,IO 开销很高。
  2. ❌:只看执行时间,忽略Rows_examined;有些 SQL 很快,但扫描几十万行,流量上涨就会雪崩。
  3. ❌:慢日志记录 SQL 不代表 SQL 一定写的差,有可能是服务器压力大、锁冲突、IO 打满导致变慢。
  4. ❌:修改 global 变量后,当前会话直接测试,需要重连会话新参数才生效。

7. 和 show processlist、performance_schema 的区别

三、拿到慢 SQL 之后标准排查优化流程

整体步骤

  1. 捕获慢 SQL:慢查询日志 /performance_schema/ APM,拿到完整 SQL 文本
  2. Explain 看执行计划,定位根因
  3. 分析:索引?SQL 写法?数据量?锁?事务?
  4. 给出优化方案,验证改写 SQL
  5. 上线,观察慢日志、QPS、CPU、IO 变化

1. Explain 重点看哪些字段

explain select * from t where name='xxx';

⚠️ explain 是预估,不是真实运行结果,优化器会根据统计信息选择索引,统计信息不准会导致 explain 结果和真实执行不一致。

2. 对应原因

实战示例

建表:

CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(32) DEFAULT NULL,
  `age` int DEFAULT NULL,
  `status` tinyint DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_status_createtime` (`status`,`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

示例 1:全表扫描type=ALL(最坏)

explain select * from user where name='zhangsan';
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEuserALLNULLNULLNULLNULL10000Using where

解读:

问题:没有索引,全表扫描,扫描全部 10000 行,在 server 层过滤数据。 解决方法

-- 给查询条件建立索引
create index idx_name on user(name);

注意:字段区分度很低不要建索引(比如性别),索引收益极小。

示例 2:range 范围扫描(风险场景)

explain select * from user where id between 1 and 8000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEuserrangePRIMARYPRIMARY4NULL8000

问题:type 虽然是 range,但是预估扫描 8000 行,扫描数据量大,依然是慢 SQL。range 不等于快。 解决方法

  1. 业务层面缩小查询范围,不要一次性查过大区间;
  2. 如果不需要全部字段,使用覆盖索引减少回表;
  3. 大数据量分页使用书签分页 id>xxx limit ...,避免大范围 between。
-- 书签分页示例
select id,name from user where id>100 limit 10;

示例 3:Using filesort 文件排序

删除 idx_status_createtime 索引后执行

explain select * from user where status=1 order by create_time desc;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEuserALLNULLNULLNULLNULL10000Using where; Using filesort

问题:查询出满足 where 的数据之后,MySQL 额外做排序操作,内存不够就落到磁盘文件排序,性能差。

解决方法

建立联合索引:where 过滤字段放前面,order by 排序字段紧跟其后。

create index idx_status_createtime on user(status,create_time);

建立索引后,索引本身已经有序,不需要 filesort。

注意:排序方向要一致,不能一个 asc 一个 desc,会失效。

示例 4:Using temporary 临时表

status 无索引:

explain select status,count(*) from user group by status;

表格

idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEuserALLNULLNULLNULLNULL10000Using temporary; Using filesort

问题:group by /distinct 需要创建临时表存储中间分组结果,开销高。 解决方法

-- group by的字段建立索引
create index idx_status on user(status);

索引有序,可以直接分组,不需要临时表。

如果业务无法建索引:减少 group by 分组基数,业务内存做聚合,不要数据库大表分组。

示例 5:Using index 覆盖索引(优秀)

explain select status,create_time from user where status=1;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEuserrefidx_status_createtimeidx_status_createtime2const500Using index

说明:查询字段全部包含在索引中,不需要回表查询主键数据,性能很高。

如何实现覆盖索引 select 后面查询的列,全部放到联合索引里。

-- 查询 status,create_time,索引包含这两个字段
KEY `idx_status_createtime` (`status`,`create_time`)

❌不要 select *,会破坏覆盖索引,触发回表。

示例 6:possible_keys 有索引,key 为 NULL(有索引但是不走)

场景:索引区分度差。status 只有 0 和 1 两个值。

explain select * from user where status=1;

possible_keys 显示idx_status,key=null,type=ALL。

问题:优化器判断走索引回表代价高于全表扫描,放弃索引直接全表扫描。 解决方法

  1. 低区分度字段不适合单独建索引;
  2. 调整业务,增加过滤条件;
  3. 可以强制索引(仅临时验证,生产慎用)
select * from user force index(idx_status) where status=1;

示例 7:索引失效(隐式类型转换)

name 是 varchar 字符串字段,查询传入数字

explain select * from user where name=1234;

即使有idx_name索引,key 依旧为 NULL,type=ALL。 原因:字段发生隐式类型转换,索引失效。 解决方法:保持类型一致

select * from user where name='1234';

示例 8:type=index(扫描整个索引树)

explain select name from user;

type=index,Extra 为空。

type=index:遍历整个二级索引树,不是全表,但扫描大量索引页,性能差;不要和Using index混淆。 解决方法:业务加 where 条件,不要不加条件查询大量数据。

Extra 字段速查表

表格

Extra现象处理方案
Using where存储引擎返回数据,服务层过滤搭配 type=ALL 才需要优化
Using filesort文件排序建立联合索引,where 条件 + order by 字段
Using temporary临时表group by/distinct 字段建索引
Using index覆盖索引✅优秀,尽量做到

3. 区分:是 SQL 本身慢,还是锁等待慢

看慢日志里 Lock_time:

4. 优化后验证

  1. explain 确认执行计划改善
  2. 线上灰度观察:慢日志不再出现这条 SQL,数据库 CPU、IO 下降,响应时间下降
  3. 关注边界:数据增长后会不会再次恶化

总结

拿到 explain 执行计划:

  1. 看type,优先保证至少达到 range/ref 级别,避免 ALL;
  2. 看key确认是否真正使用索引,possible_keys 有索引不等于实际使用;
  3. 看rows预估扫描行数,行数过大即使 type 不错也是风险 SQL;
  4. 看 Extra,出现Using filesort、Using temporary需要优化;
  5. 判断是缺少索引、索引失效、优化器放弃索引;针对性建索引、改写 SQL;
  6. 优化完再次 explain 验证。

注意:rows 是预估值,MySQL 统计信息不准,explain 结果不等于真实执行情况。

到此这篇关于MySQL慢查询日志的文章就介绍到这了,更多相关MySQL慢查询日志内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文