MySQL慢查询优化实战:帮客户把接口响应从5秒降到50ms

MySQL慢查询优化实战:帮客户把接口响应从5秒降到50ms
帮客户把接口响应从5秒降到50ms的MySQL慢查询优化实录前两天帮一个做供应链管理的客户修了个线上Bug他们那个库存同步接口平时跑得挺稳但最近不知道怎么回事高峰期响应时间直接飙到5秒以上前端页面卡得连转圈都转不动了。我接手的时候对方运维 guys 已经急得跳脚说要是再降不下来老板就要砍预算换云数据库了。说实话这种场景我太熟了一看就是 SQL 没写好或者索引失效。我赶紧让他们导出了最近的慢查询日志打开一看好家伙一条SELECT语句居然跑了 4.8 秒这还没算网络延迟纯数据库耗时。索引失效的真相与实战抉择日志里那条罪魁祸首的 SQL 长这样sqlSELECT id, sku_code, warehouse_id, quantityFROM inventory_recordsWHERE sku_code SKU_2024_A89AND create_time BETWEEN 2024-01-01 AND 2024-01-31ORDER BY create_time DESCLIMIT 10;表里有大概 5000 万行数据。当时我第一反应是看看sku_code有没有索引查了一下EXPLAIN发现虽然有个联合索引(sku_code, create_time)但因为create_time的范围查询导致了索引失效MySQL 走了全表扫描。这时候摆在我面前的有两个方案方案 A 是把create_time改成精确匹配比如只查某一天但这显然不符合业务需求业务方就是要查一个月的数据方案 B 是调整索引顺序把范围查询的字段放在联合索引的最后面让前缀索引能最大化利用。当时我觉得方案 A 简单粗暴改代码加个天级别的时间戳过滤就行结果发现不对业务逻辑里确实需要跨天查询。试了一圈发现方案 B 才是正解。我把索引改成了(create_time, sku_code, warehouse_id)这样create_time的范围扫描能快速定位到大致区间然后在这个小范围内通过sku_code进行精确过滤。这里有个坑我一开始以为改了索引就能立马生效结果EXPLAIN还是显示用了 filesort。仔细一查原来是因为ORDER BY的字段不在索引的前导列中而且LIMIT 10导致优化器觉得回表开销比排序还大干脆就不走索引了。有意思的是MySQL 5.7 之后对这种场景其实有优化但在 8.0 版本下如果数据分布不均匀优化器可能会“自作聪明”地选错计划。为了解决这个问题我强制加了FORCE INDEX测试了一下果然快了十倍。但生产环境不能靠硬编码FORCE INDEX太脆弱了。于是我做了一个折中决策保持现有索引结构不变而是通过覆盖索引来优化。我在原索引基础上增加了一个包含所有查询字段的复合索引(sku_code, create_time, quantity)并且去掉了warehouse_id因为这个字段在业务上是必查项且基数较小。sql-- 新创建的覆盖索引ALTER TABLE inventory_records ADD INDEX idx_sku_time_qty (sku_code, create_time, quantity);改成这样后查询可以直接从索引树中拿到所有需要的数据不需要回表。这一步操作让我把原本需要 4.8 秒的查询压缩到了 0.2 秒左右。但我还没松口气因为 0.2 秒对于用户感知来说还是有点慢目标可是 50ms 啊。深度优化与最终落地既然已经消除了回表瓶颈就在哪里了呢我抓包看了下发现每次请求返回的数据里quantity字段是BIGINT而且create_time是DATETIME。虽然这些数据不大但在高并发下序列化这些对象也是有开销的。不过真正的问题出在另一个地方——分页。客户那边的前端实现有个典型错误每次翻页都重新计算偏移量当翻到第 100 页时SQL 变成了LIMIT 1000, 10。对于 5000 万数据量的表即使有索引MySQL 还是要跳过前 1000 条数据。更糟糕的是之前的EXPLAIN显示虽然用了索引但因为ORDER BY create_time DESC配合LIMITMySQL 需要维护一个大小为 10 的小顶堆这在数据倾斜严重时也会拖慢速度。我当时觉得这样就行结果发现错了。单纯的覆盖索引并没有解决深分页的性能抖动问题。我决定引入“游标分页”的概念也就是基于上次查询的最大create_time和id进行过滤而不是使用OFFSET。修改后的 SQL 变成了这样sqlSELECT id, sku_code, warehouse_id, quantityFROM inventory_recordsWHERE sku_code SKU_2024_A89AND create_time last_max_create_timeAND (create_time last_max_create_time OR id last_max_id)ORDER BY create_time DESC, id DESCLIMIT 10;这个改动看似简单实则颠覆了之前的查询逻辑。我帮前端同事一起重构了分页组件把原来的page2传参方式改成了传递cursor值。说实话刚开始推这个方案时阻力挺大的因为业务方习惯了传统分页觉得改起来麻烦。但我坚持解释了深分页在大数据量下的性能灾难最终他们同意了。改造完成后我再跑了一次压测。最惊人的是无论翻到哪一页响应时间都稳定在 30-50ms 之间。这不仅仅是索引优化的功劳更是因为消除了OFFSET带来的随机 I/O 和排序开销。还有一个小插曲我发现在某些极端情况下如果某个月份的数据量极大比如双 11 期间单月数据超过 500 万即使有索引范围扫描的成本依然很高。这时候我考虑过按月分区但考虑到分区的维护成本和复杂度以及客户目前的业务增长曲线我觉得暂时没必要。如果未来单月数据超过 2000 万我会再回来做物理分区。这次优化让我深刻体会到SQL 优化不是玄学而是对执行计划和数据分布的精准把控。从 5 秒到 50 毫秒差的不是一个索引而是对整个查询链路的重塑。本文基于实际项目经验整理如有技术问题欢迎私信交流。

最新新闻

日新闻

周新闻

月新闻