云计算百科
云计算领域专业知识百科平台

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

概念

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

一、慢 SQL

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

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

判定标准

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

  • 执行耗时:超过 long_query_time 阈值(比如 1s),慢查询日志会捕获。
  • 扫描行数 (Rows_examined):扫描大量数据,哪怕返回很少结果、耗时很短,依然属于潜在慢 SQL。
  • 举例:一条 SQL 执行 0.4 秒,没达到 1s 阈值不会进慢日志,但扫描 20 万行。数据量增长后,未来会变成严重慢 SQL,这就是隐形慢 SQL,最容易被忽略。

    慢 SQL 常见成因

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

    • 没建索引,发生全表扫描
    • 索引失效:隐式类型转换、like %xxx、or 条件、对索引列做函数运算、not in 等
    • 索引选择错误:优化器选错索引
    • 索引区分度差,低选择性索引

    2. SQL 写法问题

    • select * 查询大量不需要字段,回表开销大,网络传输量大
    • 大表 join,关联字段无索引
    • 不合理子查询,衍生表没有索引
    • limit 大偏移:limit 100000,10,扫描前面十万行
    • in 里面放成千上万的值
    • order by /group by 没有合适索引,触发文件排序、临时表

    3. 业务 & 数据层面

    • 单表数据量过大,千万级别以上,没有分库分表
    • 事务过大:一个事务里面执行大量 DML,锁持有时间长,导致大量 SQL 阻塞等待锁
    • 批量一次性操作海量数据

    4. 数据库环境因素

    • IO 压力高、CPU 打满,再好的 SQL 也跑慢
    • 锁等待:不是 SQL 本身慢,是等行锁 / 表锁,Lock_time很高
    • buffer pool 太小,大量数据读磁盘而不是内存

    慢 SQL 带来的危害

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

  • 慢查询日志 slow_query_log:事后记录已经执行完的慢 SQL
  • show processlist / show full processlist:抓正在运行的慢 SQL
  • performance_schema:线上持续监控 SQL 执行指标
  • APM 工具(SkyWalking 等):应用侧上报慢 SQL
  • 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';

    字段释义:

  • Query_time:SQL 实际执行耗时 2.34s(核心指标)
  • Lock_time:等待锁的时间
  • Rows_sent:返回给客户端行数
  • Rows_examined:扫描的行数,这个值很大代表扫描大量数据,是优化重点
  • 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

    参数说明:

    • -s:排序方式
      • t:Query_time 查询时间
      • r:Rows_sent 返回行数
      • a:平均查询时间
    • -t:top N,输出多少条
    • -g:正则过滤 SQL

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

    第三方工具

    • pt-query-digest(percona toolkit):工业级慢日志分析,比 mysqldumpslow 强大,支持分析文件、binlog、general log,输出报告包含占比、次数、平均耗时。生产环境最常用。

    5. 生产最佳实践

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

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

    • 慢查询日志:事后记录,已经跑完的慢 SQL;
    • show processlist:实时看当前正在运行 SQL;
    • performance_schema:可以做更细粒度监控,不需要文件日志,适合线上持续监控。

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

    整体步骤

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

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

    • type:访问类型,性能从优到劣:system > const > eq_ref > ref > range > index > ALL
      • ALL:全表扫描,重点优化
      • range:范围扫描,要看扫描行数是否过大
    • key:实际使用的索引,NULL 代表没用到索引
    • rows:预估扫描行数,数值越大越危险
    • Extra:额外信息
      • Using filesort:文件排序,需要磁盘排序,性能差,要建立联合索引避免
      • Using temporary:使用临时表,常见 group by /distinct,开销大
      • Using index:覆盖索引,很好,不需要回表
      • Using where:存储引擎返回数据后,MySQL 服务层再过滤

    ⚠️ 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
    1 SIMPLE user ALL NULL NULL NULL NULL 10000 Using where

    解读:

    • type=ALL:全表扫描
    • possible_keys=NULL:没有可选索引
    • key=NULL:没使用索引
    • rows=10000:预估扫描 10000 行
    • Extra:Using where:引擎把全部数据返回给 MySQL 服务层,在服务层做name='zhangsan'过滤。

    问题:没有索引,全表扫描,扫描全部 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
    1 SIMPLE user range PRIMARY PRIMARY 4 NULL 8000

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

  • 业务层面缩小查询范围,不要一次性查过大区间;
  • 如果不需要全部字段,使用覆盖索引减少回表;
  • 大数据量分页使用书签分页 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
    1 SIMPLE user ALL NULL NULL NULL NULL 10000 Using 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
    1 SIMPLE user ALL NULL NULL NULL NULL 10000 Using 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
    1 SIMPLE user ref idx_status_createtime idx_status_createtime 2 const 500 Using 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。

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

  • 低区分度字段不适合单独建索引;
  • 调整业务,增加过滤条件;
  • 可以强制索引(仅临时验证,生产慎用)
  • 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:

    • Lock_time 大,Query_time 不大:SQL 本身很快,大量时间在等锁,属于锁冲突问题,不是 SQL 索引问题。 排查方向:大事务、长事务,行锁冲突。
    • Lock_time 很小,Query_time 很大:SQL 执行本身慢,优先优化索引和 SQL 写法。

    4. 优化后验证

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

    拿到 explain 执行计划:

  • 看type,优先保证至少达到 range/ref 级别,避免 ALL;
  • 看key确认是否真正使用索引,possible_keys 有索引不等于实际使用;
  • 看rows预估扫描行数,行数过大即使 type 不错也是风险 SQL;
  • 看 Extra,出现Using filesort、Using temporary需要优化;
  • 判断是缺少索引、索引失效、优化器放弃索引;针对性建索引、改写 SQL;
  • 优化完再次 explain 验证。
  • 注意:rows 是预估值,MySQL 统计信息不准,explain 结果不等于真实执行情况。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 详解MySQL 慢查询日志( 附详细案例 )
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!