一次慢 SQL 的排查手记:从执行计划到索引失效
上周三上午,一条订单列表查询接口突然变慢:平时 80 毫秒返回,峰值时却要 4 秒以上,超时告警接连触发。应用日志里没有异常,CPU 和内存也都在正常水位,问题看起来就藏在数据库里。本文记录这次排查的完整过程,以及从中学到的几个关于索引的教训。
一、先复现,再猜测
遇到慢查询,第一反应往往是“加个索引就好了”,但在动手之前,先把问题复现出来更重要。我在测试环境用同样的参数跑了这条 SQL,耗时同样超过 3 秒,说明问题可以稳定复现,与线上流量、连接池、缓存抖动关系不大。
接着,我打开了数据库的慢查询日志,确认这条 SQL 的平均耗时、扫描行数和返回行数。扫描行数接近 40 万,返回行数只有 20 行,两者差距悬殊,这通常意味着查询走了低效的扫描路径,或者索引没有起到过滤作用。
二、看执行计划,找真正的瓶颈
执行计划是排查慢 SQL 最直接的工具。我用 EXPLAIN 查看了这条查询,关键信息有三点:第一,type 是 ALL,意味着全表扫描;第二,possible_keys 里明明列出了一个索引,但 key 一栏却是空的,优化器最终没有选择它;第三,rows 估算值很大,Extra 里出现了 Using where 和 Using filesort。
全表扫描加文件排序,4 秒的耗时就解释得通了。但为什么优化器放着索引不用?这是下一步要回答的问题。
三、索引失效的几种常见原因
我逐一核对了这条 SQL 的写法,发现了两个问题。
第一个问题是函数操作。查询条件里对时间字段用了 DATE_FORMAT 做格式化比较,比如 DATE_FORMAT(create_time, ‘%Y-%m-%d’) = ‘2026-11-04’。一旦对索引列套上函数,数据库就无法直接利用索引的有序性,只能逐行计算后再比较,索引随之失效。改成范围比较 create_time >= ‘2026-11-04’ AND create_time < ‘2026-11-05’ 之后,执行计划立刻变了。
第二个问题是隐式类型转换。订单号字段在表里是 VARCHAR 类型,但查询时传入的是数字,数据库为了比较,不得不把每一行的字符串转成数字再比对,索引同样失效。统一参数类型、改成字符串传入后,这个问题也消失了。
除了这两个,实践中还常见几种索引失效的场景:对索引列使用 LIKE ‘%xxx’ 前缀模糊匹配、最左前缀原则被破坏、OR 条件两侧字段没有同时建索引、以及数据分布极不均匀时优化器主动放弃索引。逐条对照,往往能定位到原因。
四、改完之后要验证,不要凭感觉
改掉函数操作和参数类型后,我重新跑了 EXPLAIN:type 变成了 range,key 命中了目标索引,rows 从 40 万降到了 120 左右,Extra 里的 filesort 也消失了。再跑实际查询,耗时从 3 秒多降到了 60 毫秒以内,和平时水平一致。
不过这还不算完。我又做了两件事:一是用线上接近真实分布的数据量再测一次,避免测试环境数据量太小掩盖问题;二是观察了一段时间的慢查询日志,确认这条 SQL 不再出现,同时关注整体 QPS 和 CPU 没有异常波动。
五、预防比救火更重要
这次排查花了大半天,但很多工作其实可以提前做。我的经验有三条:
第一,上线前对核心 SQL 做 EXPLAIN 评审,重点关注 type、key、rows 和 Extra 四个字段,把全表扫描挡在发布之前。
第二,慢查询日志要定期看,而不是出了故障才翻。把“扫描行数远大于返回行数”的 SQL 列为重点关注对象,它们往往是下一个故障点。
第三,写 SQL 时养成两个习惯:避免对索引列做函数运算和隐式转换,条件字段的类型与表定义保持一致。这些习惯看起来琐碎,却能挡住大部分索引失效问题。
慢 SQL 很少是数据库本身的问题,更多是写法与数据结构之间的不匹配。学会读执行计划、理解优化器的选择逻辑,排查就有了章法;把这些检查前置到开发和评审环节,才能少在深夜被告警叫醒。
- 点赞
- 收藏
- 关注作者
评论(0)