上周接口开始零星报超时,量不大但一直有。排查下来是一条写了很久的老 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 取前十条。它会把具体参数值抽象成 N 和 S,相同结构的 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;
关键几列:
| 列 | 值 | 说明 |
|---|---|---|
| type | ref | 用到了索引,但不是最优 |
| key | idx_shop_id | 只命中了单列索引 |
| rows | 1823400 | 预估扫描行数 |
| Extra | Using where; Using filesort | 回表过滤 + 额外排序 |
问题清楚了:表上只有 shop_id 单列索引,这个店铺本身就有一百多万单,索引筛完之后 status 和 created_at 两个条件全靠回表逐行判断,最后还要对结果做一次 filesort 排序。
Using filesort不一定意味着写磁盘,数据量小的时候在内存里排。但它说明排序没有走索引顺序,数据量一大就会溢出到临时文件。
四、建联合索引,顺序是关键
三个字段的先后顺序直接决定索引能用到第几列。MySQL 的联合索引遵循最左前缀原则,范围查询之后的列无法再用于索引查找。
所以顺序应该是:等值条件在前,范围条件在后,排序字段紧跟其后。
ALTER TABLE orders
ADD INDEX idx_shop_status_created (shop_id, status, created_at);
这样 shop_id 和 status 两个等值条件先把范围缩到很小,created_at 既能用于范围过滤,其有序性又正好满足 ORDER BY created_at DESC,filesort 也一并省掉了。
如果当初把 created_at 放在中间,范围查询之后 status 就用不上索引了,效果会差很多。
重新 EXPLAIN:
| 列 | 调整前 | 调整后 |
|---|---|---|
| key | idx_shop_id | idx_shop_status_created |
| rows | 1823400 | 21 |
| Extra | Using where; Using filesort | Using 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 看清为什么慢 → 针对性建索引 → 验证扫描行数。中间任何一步都不要跳过直接猜,尤其不要一上来就加索引,很容易加出一堆没用的。