SQL 查询突然变慢了?一篇讲清慢 SQL 排查思路

索引失效那篇讲的是"为什么没走索引",讨论的是 SQL 写法本身。但线上遇到的"某条 SQL 突然变慢了",原因不止没走索引一种,而且它可能根本不是 SQL 的问题------是被锁堵住了。这篇把排查的顺序理一遍:先确认慢在哪,再看执行计划,最后按原因分类。


第一步:拿到慢的是哪条

改 SQL 之前得先知道改哪条,靠的是慢查询日志。

sql 复制代码
show variables like 'slow_query_log%';
show variables like 'long_query_time';

三个关键参数:

参数 作用 默认
slow_query_log 开关 OFF
long_query_time 超过多少秒算慢 10 秒
slow_query_log_file 日志文件路径 数据目录下
sql 复制代码
-- 不用重启就能开
set global slow_query_log = on;
set global long_query_time = 1;

注意 long_query_time 的默认值是 10 秒,而线上大部分慢 SQL 在几百毫秒的量级,按 10 秒去抓基本什么都抓不到,一般调成 1 秒甚至 0.x 秒。

日志本身是一行行的原始记录,量大了没法直接看,用工具聚合成"哪类 SQL 排第一、执行了多少次、平均耗时、扫描行数":

shell 复制代码
mysqldumpslow -s t /var/lib/mysql/slow.log     # 按总耗时排序
pt-query-digest /var/lib/mysql/slow.log        # 报告更细,推荐

除了历史日志,show processlist 能看到当前正在执行的语句和它的 state,用来抓"正在发生"的慢查询。这里的 state 后面判锁还要用到。


第二步:看执行计划

锁定语句之后,EXPLAIN 是绕不开的:

sql 复制代码
explain select * from t where a = 1 and b > 2;

输出里重点就四列:

列 看什么
type 访问方式,ALL 全表扫描最差;由好到坏 const > eq_ref > ref > range > index > ALL
key 实际用的索引,NULL 就是没走
rows 预估扫描行数,和实际差太远说明统计信息不准
Extra 额外信息,几处坑都在这

Extra 里几个常见的值:

值 含义
Using filesort 排序没走索引,额外做了一次排序
Using temporary 建了临时表,常见于 group by、distinct
Using index 覆盖索引,要的列都在索引里,不用回表,是好事
Using index condition 用上了索引下推
Using where 拿回来的行还要再过滤一遍

Using filesort 和 Using temporary 是两个明显信号,看到就该查这里是不是缺了对应索引。这条线索和 索引失效那篇里读 EXPLAIN 的部分是同一个。


按原因分类

没走索引

最常见,也最好办。条件写法破坏了 B+ 树的顺序,或者优化器算下来觉得全表更划算。具体分哪几种、怎么改写,都在 索引失效的场景总结 里,这里不重复。

走了索引,但走错了

key 列有值,不是 NULL,但选的不是你以为的那个索引。

原因是优化器靠统计信息 估代价,统计信息不准就会选错。rows 明显偏离实际、或者 possible_keys 里有更合适的索引却没被选中,基本都是这个情况。

sql 复制代码
analyze table t;    -- 重新收集统计信息

多数情况下 analyze 完就好了。还不行可以用 force index 硬指定,但这属于打补丁,多半是统计信息或者 SQL 设计本身有问题,别当常规手段。

扫的行不多,但回表太多

rows 很小,type 也是 ref,可就是慢。这时要看 select 的列------如果查的列不在索引里,每命中一行就要回一次主键索引,也就是回表。命中一万行就是一万次随机 IO。

两个方向:把要查的列放进索引做成覆盖索引(Extra 会显示 Using index),或者干脆别 select *,只查真正要用的列。

深分页

sql 复制代码
select * from t order by id limit 1000000, 10;

这条 SQL 只想要 10 行,但 MySQL 得先扫出前 1000010 行、再把前面的丢掉。limit 的偏移量越大越慢,耗时和数据量成正比。

两种改法:

sql 复制代码
-- 游标:记住上一页最后一条的 id,下一页从它往后取
select * from t where id > 1000000 order by id limit 10;

-- 延迟关联:先只在索引上翻页拿到主键,再回表取这 10 行
select * from t
join (select id from t order by id limit 1000000, 10) x using (id);

游标的办法最快,但要前端配合;延迟关联不用改接口,把回表推迟到最后那 10 行上。

join 太多,驱动表选错

多表 join 时优化器要决定以哪张表为驱动表。驱动表越大、被驱动表上没索引,代价就越离谱。EXPLAIN 有多行输出,table 列的顺序就是执行顺序,重点看被驱动表的 type 是不是 ALL。

join 的表数也建议控制住,MySQL 对 join 的优化能力有限,超过三张表就该考虑是不是 SQL 设计本身要拆。

不是 SQL 慢,是被锁堵住了

这一类最容易被误判。SQL 本身写得没问题,平时也很快,某段时间突然慢------因为它要改的行正被别的事务占着,一直在等锁。

判断办法是看 state:

sql 复制代码
show processlist;

如果状态是 Waiting for table metadata lock、Waiting for row lock 之类,就不是执行计划的问题。再看谁占着锁:

sql 复制代码
select * from information_schema.innodb_trx order by trx_started;

trx_started 很早的记录就是长事务的嫌疑对象。长事务的危害在 Undo Log 那篇里讲过,它不只堵住别人,还会让 undo 一直清不掉。

一个区分技巧:慢查询日志里如果某条语句 rows_examined 很小、rows_sent 也很小,却耗了好几秒,基本可以断定在等锁------扫描不到几行不可能慢,时间都花在等待上。

环境变了

前面几条都是针对单条 SQL 的,还有一类慢是整个环境造成的,SQL 一个字没改。

变化 表现
数据量涨了 原来几千行走索引很快,涨到几百万行扫描代价变了,优化器可能改走全表
buffer pool 装不下 热数据进不了内存,每次查都落磁盘随机 IO,innodb_buffer_pool_size 太小
机器负载高 CPU、磁盘 IO 被别的进程占满,SQL 本身没问题但响应变慢
并发上来了 单条不慢,一起跑就慢,锁竞争和资源竞争

这一类要在 SQL 之外找,看监控里的 QPS、连接数、磁盘 IO、buffer pool 命中率。


排查顺序

串成一条路,遇到慢 SQL 按这个顺序走,能少绕弯:

  1. 先分清是"执行慢"还是"被堵住" 。看 show processlist 的 state,或者在慢日志里看扫描行数和耗时是否成正比。被锁的先去解决锁,改 SQL 没用。
  2. 再拿 EXPLAIN 看计划 。type = ALL 或者 key = NULL,进"没走索引";key 有值但选错了,看统计信息;rows 小但还是慢,看回表和排序(Extra)。
  3. SQL 本身没问题就往外看 。数据量、buffer pool、机器负载、并发。

大部分线上慢 SQL 落在第 2 步,而第 2 步里的绝大多数又是"没走索引",也就是索引失效那篇里列的那些场景。

相关推荐
花间相见1 小时前
【计算基础|MySQL03】【上手】多表查询与聚合
数据库·mysql
小羊没烦恼!1 小时前
T-SQL查询高级—SQL Server索引中的碎片和填充因子
开发语言·前端·javascript·数据库·后端·python·web安全
the局外人1 小时前
学会 SQLAlchemy 搭建 Python 与 MySQL 之间的数据访问桥梁 🚀
后端·python·sql
冰暮流星1 小时前
mysql索引失效的情形
数据库·笔记·sql·mysql
金銀銅鐵2 小时前
[Java] 用 GUI 展示 ArrayList 扩容的过程
java·后端·ai编程
Pniubi2 小时前
三种count
mysql
霸道流氓气质2 小时前
基于 Spring @Profile 的 MQ 消费者按需开关设计
java·后端·spring
ynchyong3 小时前
创建MySQL只读账号
android·数据库·mysql