CPU 100% 定位实录:一条正则引发的回溯风暴

举报
yd_293030668 发表于 2026/11/05 14:31:38 2026/11/05
【摘要】 一条 8 秒的慢 SQL,我是这样优化到 50ms 的上周排查一个后台导出接口的超时问题,定位到一条 SQL 上。执行时间 8.3 秒,数据量并不大(表里就 40 万行),慢得有点离谱。整个过程记录一下,顺便把 MySQL 慢查询排查的套路理一理。 现象接口每隔几分钟就有一次超时(网关限的 10 秒),日志里对应的 SQL 大致是这样:SELECT o.id, o.order_no, o....

一条 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

问题在这两处:

  1. idx_status 命中后要扫 18000 行——因为 status=3 的记录占比很高(大部分订单都完成了),这个索引区分度太差
  2. 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 排查我总结了四步:

  1. 慢日志确认现象——看 Query_time、Rows_examined、Rows_sent 三个数的关系
  2. EXPLAIN 看计划——重点关注 type、key、rows、Extra 里的 filesort / temporary
  3. 针对性建索引——等值在前、排序在后,让索引同时覆盖过滤和排序
  4. 验证 + 评估代价——再看 EXPLAIN、压测确认,并考虑写入影响

这次的核心就一句话:把 ORDER BY 和 WHERE 一起塞进复合索引,filesort 消失了,8 秒变 50 毫秒。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。