查询优化案例复盘:从线上故障到长效保障‌

📅 2026/7/22 14:36:37 👁️ 阅读次数 📝 编程学习
查询优化案例复盘:从线上故障到长效保障‌

查询优化案例复盘:从线上故障到长效保障‌

做数据库开发和运维的人,几乎都遇到过这样的惊魂时刻:大促前压测一切正常,零点刚过流量峰值一到,核心订单查询接口突然大面积超时,数据库CPU直接冲到100%,几十分钟都降不下来,最后只能靠限流降级勉强保住核心链路,事后复盘翻遍慢查询日志,才发现是一条之前被忽略的低频查询突然拖垮了整个库。很多团队做SQL优化都是“兵来将挡水来土掩”,出一个慢SQL就临时改一个,从来没有把线上真实故障沉淀成可复用的优化方法论,最后同样的坑反复踩,同样的故障反复发生。其实查询优化从来不是靠零散的技巧堆出来的,它是一套从问题定位、根因分析到落地验证、长效保障的完整工程体系。今天我就把过去7年在电商平台经历的6个真实线上查询优化案例完整复盘出来,每一个案例都包含故障现场、排查过程、优化方案和事后长效机制,帮你把别人踩过的坑变成自己的优化经验库,以后遇到同类问题能快速定位解决,再也不会在大促高峰期手忙脚乱。

一、千万级订单表深分页查询超时故障

这个案例发生在2023年618大促前的最后一轮压测,当时运营后台的订单导出接口在翻到第100页之后直接超时,单条SQL的执行耗时超过30秒,直接把从库的磁盘IO打满,影响了所有依赖从库的查询接口。

故障发生的时候,我们第一时间从慢查询日志里捞出来这条问题SQL,它的逻辑非常简单:按创建时间倒序分页查询订单列表,用的是最常见的LIMIT 20000,20写法,也就是跳过前20000条数据,返回第20001到20020条订单。一开始我们以为是没有给create_time建索引,但是用Explain一看,这条SQL明明已经走了create_time的二级索引,type字段是range,看起来完全没问题,但是实际扫描的行数超过了20020行,性能完全达不到预期。

我们顺着执行逻辑往下深挖才发现问题的根因:MySQL执行LIMIT 20000,20的时候,并不是直接跳到第20000行的位置取20条数据,而是要先从头到尾扫描前20000条完全无用的数据,直接把它们全部丢弃之后,才会返回后面的20条结果。当分页深度到第1000页的时候,就要扫描20万行无关数据,哪怕走了索引,大量的随机IO也会把磁盘性能拖垮。

我们最终落地的优化方案是用“子查询主键定位法”改造SQL,先在覆盖索引里快速定位到第20000条数据的主键ID,再通过这个ID往后取20条订单数据,全程不需要扫描前面的20000行冗余数据。优化前后的SQL对比如下:

sql

-- 优化前的深分页写法

SELECT * FROM order_info

WHERE create_time >= '2023-06-01'

ORDER BY create_time DESC

LIMIT 20000,20;

-- 优化后的深分页写法

SELECT a.* FROM order_info a

INNER JOIN (

SELECT id FROM order_info

WHERE create_time >= '2023-06-01'

ORDER BY create_time DESC

LIMIT 20000,1

) b ON a.id = b.id

ORDER BY a.create_time DESC

LIMIT 20;

优化之后这条SQL的执行耗时直接从30秒降到了15毫秒,哪怕翻到第1000页,性能也能保持稳定。事后我们做了长效保障:给所有运营后台的分页接口统一增加最大页码限制,不允许超过100页,同时全量扫描所有深分页SQL,全部改成主键定位的优化写法,从根源上避免同类问题再次发生。

二、隐式类型转换引发的全表扫描雪崩

这个案例发生在2024年年初的一次日常版本上线,上线之后10分钟,数据库的CPU使用率突然从平时的20%直接冲到95%,大量用户的支付查询接口超时,线上告警直接炸了几百条。

我们第一时间登上数据库服务器,用show processlist看当前的活跃线程,发现有几百条完全一样的查询卡在执行状态,这条SQL的逻辑是根据支付流水号查询支付记录,开发人员明明给pay_flow_no字段建了唯一索引,但是实际执行的时候完全没有走索引。我们立刻用Explain分析这条SQL,发现type字段是ALL,key字段为空,预估扫描行数是800万,完全是全表扫描的状态。

排查了十几分钟才找到根因:pay_flow_no字段在数据库里定义的是varchar字符串类型,但是代码里传入的参数是Long长整型,MySQL在执行的时候触发了隐式类型转换,自动把每一行的pay_flow_no字段转成数字和传入的参数做比较,导致索引完全失效,所有请求都在做全表扫描,瞬间把CPU打满。

我们的应急方案非常简单,立刻在代码里把传入的参数改成字符串类型,重新发布之后,所有请求立刻恢复走唯一索引,单条SQL耗时从原来的2秒降到了1毫秒,数据库CPU在1分钟之内就恢复到了正常水平。事后我们做了两层长效保障:第一层是在公司的代码扫描工具里增加规则,只要出现查询条件参数类型和数据库字段类型不匹配的情况,直接拦截不让上线;第二层是全量扫描所有历史SQL,把所有存在隐式类型转换风险的语句全部整改,彻底杜绝同类故障再次发生。

三、多表关联驱动表选择错误引发的慢查询风暴

这个案例发生在2022年的双11大促当天,用户中心的订单关联查询接口突然大面积变慢,原本几毫秒就能返回的接口,耗时突然涨到了几百毫秒,直接拖慢了整个用户中心的响应速度。

我们拿到问题SQL之后发现,这条SQL要关联三张表:千万级的订单表order_info、10万级的用户表user_info、50万级的商品表goods_info,逻辑是查询某个用户最近的10条订单,同时关联查询对应的用户昵称和商品名称。一开始我们以为是关联字段没有建索引,但是检查之后发现所有关联字段都已经建了索引,用Explain看执行计划才发现,MySQL的优化器错误地选择了千万级的订单表作为驱动表,先扫描了整个订单表的所有数据,再关联用户表和商品表,总扫描行数超过了1000万,性能自然极差。

根因是优化器基于统计信息做判断的时候,错误估算了订单表的符合条件的行数,最终做出了完全错误的驱动表选择。我们的优化方案是用STRAIGHT_JOIN语法强制指定小表作为驱动表,让数据量最小的用户表先执行,拿到用户的信息之后,再去关联订单表和商品表,这样每次关联都能通过索引快速定位数据,总扫描行数直接降到了几十行。

优化之后的SQL耗时从原来的800毫秒降到了3毫秒,接口的吞吐量直接提升了200多倍。事后我们做了长效保障:所有超过两张表关联的核心SQL,都不允许让优化器自由选择驱动表,必须手动指定驱动顺序,避免优化器因为统计信息偏差做出错误判断,同时每季度定期更新所有核心表的统计信息,保证优化器拿到的数据分布是准确的。

四、低区分度字段索引失效引发的批量慢查询

这个案例发生在2023年的一次日常运营活动,运营人员临时加了一个查询条件,要筛选所有“待发货”状态的订单,结果这条SQL一上线,就直接把从库的磁盘IO打满,大量订单查询接口超时。

我们分析这条SQL的执行计划发现,order_status字段只有3个枚举值,区分度不到万分之一,开发人员给这个字段单独建了一个普通二级索引,但是优化器判断走索引的开销比全表扫描还大,直接放弃了索引选择全表扫描,每次查询都要扫描整个订单表的1000万行数据。当时同时有几百个这样的请求在执行,磁盘IO直接被打满。

我们的优化方案不是删掉这个索引,而是把user_id字段和order_status字段组合起来,做成(user_id,order_status)的联合索引,这样索引的整体区分度变得非常高,优化器就会愿意选择这个索引,查询某个用户的待发货订单的时候,直接通过索引就能快速定位数据,完全不需要全表扫描。

优化之后这条SQL的耗时从原来的5秒降到了2毫秒,磁盘IO使用率直接从100%降到了10%以下。事后我们做了长效保障:制定索引设计规范,明确禁止给区分度低于1%的字段单独建二级索引,所有低区分度字段必须和高区分度字段组合成联合索引使用,同时全量扫描所有历史索引,把所有不符合规范的低区分度单列索引全部整改。

五、大表加索引引发的锁表故障

这个案例发生在2022年的一次版本迭代,开发人员为了优化一个慢SQL,直接在千万级的订单表上执行ALTER TABLE语句加索引,结果执行命令之后,整个订单表直接被锁住,所有写入请求全部卡住,订单提交接口大面积超时,持续了整整40分钟,造成了不小的资损。

故障发生的时候我们才反应过来,开发人员完全忘记了MySQL的DDL操作在旧版本里会锁全表,加索引的过程中,整个表的所有读写请求都会被阻塞,千万级的大表加索引,DDL执行时间要几十分钟,这段时间整个业务完全不可用。

我们的应急方案是立刻终止正在执行的ALTER TABLE命令,然后改用pt-online-schema-change工具做无锁的索引添加,这个工具会创建一张和原表结构一样的新表,在新表上建好索引,然后把原表的数据分批拷贝到新表里,最后通过原子性的rename操作替换表,整个过程几乎不会锁表,对线上业务的影响几乎可以忽略不计。最终我们用这个工具花了15分钟就完成了索引的添加,全程没有阻塞任何线上写入请求。

事后我们做了非常严格的DDL规范:所有超过100万行的大表,绝对不允许直接在线执行ALTER TABLE做DDL操作,必须使用pt-online-schema-change或者Online DDL的方式执行,而且所有大表DDL必须安排在凌晨业务低峰期执行,提前提交变更申请,经过DBA审核通过之后才能上线,从根源上杜绝大表锁表故障再次发生。

六、分组查询临时表溢出引发的数据库OOM

这个案例发生在2024年的一次月度报表生成任务,运营人员跑了一个按天统计全平台订单金额的SQL,结果这条SQL一执行,数据库的内存使用率直接冲到100%,没过多久数据库进程就被操作系统OOM Killer杀掉,整个数据库直接宕机。

我们事后分析这条SQL的执行计划发现,它没有给create_time字段建合适的索引,执行GROUP BY create_time的时候,MySQL无法利用索引的有序性完成分组,只能在内存里创建临时表来存储分组的中间结果,但是内存临时表的大小是有限制的,当数据量超过配置的阈值之后,临时表就会转成磁盘临时表,大量的磁盘IO和内存占用最终耗尽了数据库的所有内存,引发OOM宕机。

我们的优化方案非常简单,给create_time字段创建普通二级索引,这样索引本身就是按时间有序排列的,MySQL遍历索引的时候,相同日期的数据已经连续排列在一起,完全不需要创建临时表,直接顺序遍历就能完成分组操作,内存占用几乎可以忽略不计。优化之后这条报表SQL的执行耗时从原来的2分钟降到了2秒,再也不会出现内存溢出的问题。

事后我们做了长效保障:把所有报表类的离线查询全部迁移到专门的分析型从库,不允许在主库上跑任何大流量的报表查询,同时配置数据库的内存使用告警,当内存使用率超过80%的时候立刻发出告警,DBA可以提前介入处理,避免出现OOM宕机的严重故障。

💡注意:本文所介绍的软件及功能均基于公开信息整理,仅供用户参考。在使用任何软件时,请务必遵守相关法律法规及软件使用协议。同时,本文不涉及任何商业推广或引流行为,仅为用户提供一个了解和使用该工具的渠道。

你在生活中时遇到了哪些问题?你是如何解决的?欢迎在评论区分享你的经验和心得!

希望这篇文章能够满足您的需求,如果您有任何修改意见或需要进一步的帮助,请随时告诉我!

感谢各位支持,可以关注我的个人主页,找到你所需要的宝贝。

博文入口:山峰哥-CSDN博客复制到【浏览器】打开即可,宝贝入口:常用软件宝贝:精品文件

作者郑重声明,本文内容为本人原创文章,纯净无利益纠葛,如有不妥之处,请及时联系修改或删除。诚邀各位读者秉持理性态度交流,共筑和谐讨论氛围~