CPU 100% 定位实录:一条正则引发的回溯风暴
一条 8 秒的慢 SQL,我是这样优化到 50ms 的
上周排查一个后台导出接口的超时问题,定位到一条 SQL 上。执行时间 8.3 秒,数据量并不大(表里就 40 万行),慢得有点离谱。整个过程记录一下,顺便把 MySQL 慢查询排查的套路理一理。
现象
接口每隔几分钟就有一次超时(网关限的 10 秒),日志里对应的 SQL 大致是这样:
SELECT o.id, o.order_no, o.amount, u.nickname
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 3
AND o.created_at >= '2026-10-01 00:00:00'
ORDER BY o.created_at DESC
LIMIT 20;
单看没什么问题。orders 表 40 万行,users 表 5 万行,条件也走了索引。但这个查询在特定时间点会慢到 8 秒,平时又要几百毫秒。这种"偶发慢"最难查。
第一步:确认是不是真的慢在 SQL
先用慢查询日志确认。看到这条 SQL 的同时,还看到它出现得非常频繁——每秒几十次。
-- 开启慢查询(如果还没开)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
慢日志里 Rows_examined 只有几百行,Rows_sent 20 行,但 Query_time 8 秒。扫描行数不多却耗时很长,通常指向两个方向:锁等待或排序/临时表。
第二步:看执行计划
EXPLAIN SELECT o.id, o.order_no, o.amount, u.nickname
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 3
AND o.created_at >= '2026-10-01 00:00:00'
ORDER BY o.created_at DESC
LIMIT 20;
关键信息:
| 表 | type | key | rows | Extra |
|---|---|---|---|---|
| o | ref | idx_status | 18000 | Using where; Using filesort |
| u | eq_ref | PRIMARY | 1 |
问题在这两处:
idx_status命中后要扫 18000 行——因为status=3的记录占比很高(大部分订单都完成了),这个索引区分度太差Using filesort:排序字段是created_at,但索引只用了status,于是 MySQL 先把 18000 行捞出来,再用 filesort 排 20 条
18000 行的 filesort 平时能扛住,一旦并发上来(每秒几十次查询),每个连接都占用排序缓冲区,CPU 和临时空间被吃满,个别查询就排队到 8 秒。
第三步:让索引"覆盖"过滤和排序
思路是建一个复合索引,把过滤条件和排序条件一起放进去:
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at DESC);
再 EXPLAIN:
| 表 | type | key | rows | Extra |
|---|---|---|---|---|
| o | range | idx_status_created | 20 | Using index condition; Backward index scan |
扫描行数从 18000 降到 20,filesort 也消失了——因为 status 等值匹配后,created_at 在索引里天然有序,取前 20 条直接定位即可,不用排序。
耗时从 8 秒降到 50 毫秒左右。
几个细节和坑
坑一:ORDER BY ... DESC 和索引顺序。 MySQL 8.0 支持降序索引(created_at DESC),能直接命中反向扫描。如果是 5.7,建 (status, created_at) 普通升序索引,优化器也能用 Backward index scan 反向读,但某些版本回退会导致索引失效。8.0 直接写 DESC 最稳。
坑二:别只看 EXPLAIN 的 rows。 我第一次优化时只加了个 idx_created (created_at),EXPLAIN 显示 rows 20 就以为好了,结果因为过滤条件 status 没进索引,实际还是要把近期订单全捞出来过滤。等值条件在前、范围/排序条件在后,这个顺序不能反。
坑三:索引选择性很低的列不该单独建索引。 status 只有几个值,单独建索引基本没用,但放进复合索引做前缀(配合后面的高选择性列)就很有价值。判断标准是看 Cardinality——SHOW INDEX FROM orders 里那个值,越接近表行数越好。
坑四:加索引前先想想写入代价。 orders 是高频写入表,每加一个索引,插入/更新都要多维护一棵 B+ 树。这次只加了一个复合索引,权衡下来可接受;如果是写多读少的表,就要慎重。
补充:为什么不是"加缓存"
同事第一反应是"给这个查询加个 Redis 缓存"。但这是条件组合特别多的列表查询——用户能按状态、时间、金额各种筛,缓存键的排列组合是爆炸的,命中率会很低,还引入数据一致性问题。索引优化才是治本。
小结
慢 SQL 排查我总结了四步:
- 慢日志确认现象——看 Query_time、Rows_examined、Rows_sent 三个数的关系
- EXPLAIN 看计划——重点关注 type、key、rows、Extra 里的 filesort / temporary
- 针对性建索引——等值在前、排序在后,让索引同时覆盖过滤和排序
- 验证 + 评估代价——再看 EXPLAIN、压测确认,并考虑写入影响
这次的核心就一句话:把 ORDER BY 和 WHERE 一起塞进复合索引,filesort 消失了,8 秒变 50 毫秒。
- 点赞
- 收藏
- 关注作者
评论(0)