#解决问题要彻底# 慢SQL治理完成后,如何防止同类问题“死灰复燃”?
一、 问题现场:一个触发全表扫描的“企业名称”查询
今天巡检系统时,发现一条耗时超过15s的慢SQL:
select count(*) as orderNum,
sum(amount) as totalAmt,
sum(case when order_status = 'SUCCESS' then amount end) as totalSuccessAmt,
sum(case when order_status = 'FAIL' then amount end) as totalFailAmt
from order
where enterprise_name = '海南XXXX有限公司'
and order_status = 'PROCESSING'
and create_time >= '2026-04-22 00:00:00'
and create_time < '2026-04-30 00:00:00';
这条语句来自交易订单查询页面。用户在页面筛选框中输入“企业名称”后,前端将这个值原样传给了后端,最终在拼接动态SQL时,enterprise_name 被直接用作查询条件。

隐患即刻暴露:
- 表规模巨大:
order表是核心交易表,已有约 6000万 条记录,且每日新增约12万条。 - 字段无索引:
enterprise_name字段上没有建立任何索引。 - 执行代价高昂:每次使用
enterprise_name进行查询,数据库都不得不进行全表扫描,在数千万行数据中逐条比对,性能急剧下降。
二、 应急修复:转换思路,利用索引
最不靠谱的方案是头疼医头,为 enterprise_name添加索引。(别笑~ 你或者曾经的你,在做着或做过类似的事情)
我与负责的开发者沟通后,我们采取了最直接的优化方案:转换查询维度。
order表中本就有 enterprise_id 字段且已建有索引,那么最优解就是将 enterprise_name 转换为对应的 enterprise_id 再进行查询。
原始动态SQL(MyBatis XML示例)如下:
<where>
<if test="query.enterpriseId != null">
and enterprise_id = #{query.enterpriseId}
</if>
<if test="query.enterpriseName != null and query.enterpriseName != ''">
and enterprise_name = #{query.enterpriseName}
</if>
... <!-- 其他条件 -->
</where>
与开发者沟通后,改造方案为:在业务逻辑层,根据传入的 enterpriseName 先查询出对应的 enterpriseId,然后通过enterprise_id 字段来查询(enterprise_id 有索引)。
开发者的修复方式是:直接移除了 XML 中 enterprise_name 的查询条件块。
<where>
<if test="query.enterpriseId != null">
and enterprise_id = #{query.enterpriseId} <!-- 仅使用ID查询 -->
</if>
... <!-- 其他条件 -->
</where>
立竿见影:查询速度从15s秒甚至超时下降到毫秒级,问题在当下被解决了。
三、 深层忧虑:今天解决了,明天呢?
问题虽然暂时解决,不过呢,站在工程层面,我更关注的是:我们如何保证,未来不会再有开发者在同一个地方摔倒?
- 场景一:一个月后,有新需求需要支持按企业名称搜索。一个新加入的同事,很可能不假思索地重新加上
AND enterprise_name = ...(甚至是AND enterprise_name LIKE ...) 的条件。 - 场景二:其他分支或历史代码中,可能还存在类似的、未被发现的
enterprise_name查询。 - 场景三:团队人员更替,后来的维护者并不知晓这个字段是“性能禁区”。
我问这个开发者,他痛快地说:“那最好就是去掉这个enterprise_name字段。” 这无疑是最彻底的方案——大表应尽量避免存储可关联查询的冗余信息。然而,删除字段涉及广泛的代码梳理和影响范围评估,属于中长期重构目标,无法一蹴而就。
那么,在完成彻底的重构之前,我们如何在代码层面建立一道“防火墙”,短期内有效规避风险?
四、 技术防护:为“性能禁区”设立醒目路标
我的方案是:不隐藏,而是“标记”并“禁用它”。在代码中,将危险的查询条件注释掉,并附上无法被忽略的强烈警告。
将原来的直接删除,改为如下形式:
<where>
<if test="query.enterpriseId != null">
and enterprise_id = #{query.enterpriseId}
</if>
... 其他条件 ...
<!-- !!! 性能警告:order 表为大表(6000W+),enterprise_name 字段无索引,禁止直接使用该字段进行查询!!! -->
<!-- !!! 必须通过 enterprise_id 进行查询。如需使用名称,请在业务层先根据 name 查询出 id。!!! -->
<!--
<if test="query.enterpriseName != null and query.enterpriseName != ''">
and enterprise_name = #{query.enterpriseName}
</if>
-->
... 其他条件 ...
</where>
这个简单动作的工程价值:
- 明确警示:任何后来者,在阅读或修改这段SQL时,都会首先看到这段醒目的警告,了解历史背景和性能禁忌。
- 防止误启用:如果需要临时测试或确有特殊场景,开发者必须主动删除注释。这个过程本身会促使他思考并评估风险,而不是无意识地复制粘贴一段“可用”的代码。
- 保留上下文:注释掉的代码本身,清晰地展示了“过去错误的样子”和“正确的字段是什么”(
enterprise_id),提供了修改范本。 - 成本极低:这是一种几乎零成本、可立即实施的防护措施。
显然,这比约定规范或者互相周知,要更有效。毕竟,这东西并不能让团队人员都一一get。对于小规模团队或者需求研发高频迭代的团队,有妻如此。
五、 结语:解决问题要彻底
解决一个线上慢SQL,是技术能力的体现;而思考如何防止它再次发生,则是工程素养的彰显。
“解决问题要彻底”,不仅意味着修复眼前的Bug,更意味着通过代码注释、流程规范、知识传承、技术工具等多重手段,在系统中建立起防止同类错误复发的“免疫机制”。
从给危险的代码块加上一个醒目的警告注释开始,我们就是在为团队积累资产,为系统的长期稳定运行铺设基石。这一个小小的举动,体现的正是对工程质量负责、对后来者负责的工匠精神。
当然,注释警告是有效的临时“路标”。从工程体系上,我们还可以努力做得更多,以追求更彻底的解决:
- 知识沉淀:将此次优化案例写入团队Wiki或技术知识库,标题可为“Order表查询规范:禁止使用enterprise_name字段”。在新人入职或技术分享时进行传达。
- Code Review 强化:在团队代码审查清单中,加入针对大表、无索引字段查询的检查项。审查者遇到涉及核心大表的SQL变更时,应特别警惕。
- 数据库监控与告警:配置数据库监控,对长时间运行的SQL进行抓取和告警。一旦再次出现针对
order.enterprise_name的慢查询,能第一时间发现并拦截。 - 推动根本重构:将“评估并从order表中移除冗余的enterprise_name字段”作为一个正式的技术债条目纳入规划。明确责任人,评估影响范围,制定最终清理计划。
当看到一些不好的代码时,会发现我还算优秀;当看到优秀的代码时,也才意识到持续学习的重要!--buguge
本文来自博客园,转载请注明原文链接:https://www.cnblogs.com/buguge/p/19952507
浙公网安备 33010602011771号