← 返回笔记列表

一次数据库慢查询的排查过程

上周接口开始零星报超时,量不大但一直有。排查下来是一条写了很久的老 SQL,数据量涨上来之后索引不再生效。过程记一下,主要是排查的顺序值得复用。

一、先确认是不是数据库的问题

接口超时的可能性太多,直接怀疑数据库容易跑偏。先看监控面板上的几条曲线:应用的 CPU 和 GC 都平稳,但数据库的活跃连接数在告警时段有明显尖峰,慢查询计数同步上涨。基本可以锁定在 SQL 上。

接着确认慢查询日志是开着的:

SHOW VARIABLES LIKE 'slow_query_log%';
SHOW VARIABLES LIKE 'long_query_time';

线上阈值设的是 1 秒。临时调整不需要重启:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.5;

注意 long_query_time 是会话级生效的,改完之后新建立的连接才会用新值,连接池里的老连接不受影响。

二、从慢日志里捞出真凶

慢日志原始文件很难直接看,用自带的 mysqldumpslow 按累计耗时排个序:

mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

-s t 按总耗时排序,-t 10 取前十条。它会把具体参数值抽象成 NS,相同结构的 SQL 自动归并,比一条条翻日志高效得多。

排在第一的就是订单列表的查询,简化后是这样:

SELECT id, order_no, amount, created_at
FROM orders
WHERE shop_id = 1024
  AND status = 2
  AND created_at >= '2026-07-01'
ORDER BY created_at DESC
LIMIT 20;

单次执行 2.3 秒,扫描行数 180 万,返回 20 行。扫描行数和返回行数差了五个数量级,这个比例本身就说明索引没起作用。

三、看执行计划

EXPLAIN SELECT id, order_no, amount, created_at
FROM orders
WHERE shop_id = 1024 AND status = 2 AND created_at >= '2026-07-01'
ORDER BY created_at DESC LIMIT 20;

关键几列:

说明
typeref用到了索引,但不是最优
keyidx_shop_id只命中了单列索引
rows1823400预估扫描行数
ExtraUsing where; Using filesort回表过滤 + 额外排序

问题清楚了:表上只有 shop_id 单列索引,这个店铺本身就有一百多万单,索引筛完之后 statuscreated_at 两个条件全靠回表逐行判断,最后还要对结果做一次 filesort 排序。

Using filesort 不一定意味着写磁盘,数据量小的时候在内存里排。但它说明排序没有走索引顺序,数据量一大就会溢出到临时文件。

四、建联合索引,顺序是关键

三个字段的先后顺序直接决定索引能用到第几列。MySQL 的联合索引遵循最左前缀原则,范围查询之后的列无法再用于索引查找。

所以顺序应该是:等值条件在前,范围条件在后,排序字段紧跟其后

ALTER TABLE orders
ADD INDEX idx_shop_status_created (shop_id, status, created_at);

这样 shop_idstatus 两个等值条件先把范围缩到很小,created_at 既能用于范围过滤,其有序性又正好满足 ORDER BY created_at DESC,filesort 也一并省掉了。

如果当初把 created_at 放在中间,范围查询之后 status 就用不上索引了,效果会差很多。

重新 EXPLAIN:

调整前调整后
keyidx_shop_ididx_shop_status_created
rows182340021
ExtraUsing where; Using filesortUsing where

实际执行从 2.3 秒降到 4 毫秒。

五、顺手记几个索引失效的常见写法

排查过程中翻了一遍其他 SQL,下面这几种写法都会让索引白建:

1. 在索引列上做运算或用函数

-- 失效
WHERE DATE(created_at) = '2026-07-01'
-- 改写
WHERE created_at >= '2026-07-01 00:00:00'
  AND created_at < '2026-07-02 00:00:00'

2. 隐式类型转换

这个最隐蔽。user_no 是 varchar,传了个数字进去:

-- 失效:MySQL 会把列转成数字再比较,等价于对列做了函数
WHERE user_no = 12345
-- 正确
WHERE user_no = '12345'

反过来数值列传字符串是没问题的,MySQL 会转换常量而不是列。方向搞反就中招。

3. 前导模糊匹配

WHERE name LIKE '%关键词%'   -- 用不上索引
WHERE name LIKE '关键词%'     -- 可以用

确实需要全模糊的,只能上全文索引或者外部搜索引擎。

4. OR 连接的条件里有非索引列

只要 OR 两边有一边没索引,整条就会退化成全表扫描。可以拆成两条 SQL 用 UNION ALL 合并。

六、上线注意

大表加索引会锁表。虽然 MySQL 5.6 之后支持 Online DDL,多数情况下加索引不阻塞读写,但仍会占用大量 IO,最好放在低峰期做,或者用 pt-online-schema-change 这类工具。

另外索引不是越多越好,每个索引都要在写入时同步维护。这次加完之后我顺手把已经被联合索引覆盖的 idx_shop_id 单列索引删掉了——联合索引的最左前缀已经能承担它的全部作用。


整体思路复盘:监控定位方向 → 慢日志找到具体 SQL → EXPLAIN 看清为什么慢 → 针对性建索引 → 验证扫描行数。中间任何一步都不要跳过直接猜,尤其不要一上来就加索引,很容易加出一堆没用的。