惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
量子位
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
J
Java Code Geeks
B
Blog
V
V2EX
博客园 - 三生石上(FineUI控件)
Blog — PlanetScale
Blog — PlanetScale
aimingoo的专栏
aimingoo的专栏
Y
Y Combinator Blog
F
Fortinet All Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
A
About on SuperTechFans
D
DataBreaches.Net
阮一峰的网络日志
阮一峰的网络日志
博客园 - Franky
H
Help Net Security
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
酷 壳 – CoolShell
酷 壳 – CoolShell
MongoDB | Blog
MongoDB | Blog
L
LangChain Blog

博客园 - buguge

交易成功还要记 3 笔账?我们直接砍到 1 笔—— 网络红包记账的一次自顶向下业务层重构 分享一次针对hardcode的小重构:不要把 package 名写死在代码里 聚合系统设计:如何为银行代付通道抽象出公共的代付接口能力 如何自定义 MyBatis枚举处理器(EnumTypeHandler):分享从 “同包同名 shadow 覆盖” 到 “官方扩展点” 的重构经历 这儿也改状态,那儿也改状态,连AI都理不清楚了… 不妨试试这个状态机good-practice 业务解耦的经典实践:从订单表解耦开票业务说起(以滴滴网约车开票为例) 从 `int` 到 `Duration`:一个缓存 API 的三次演进教会我的事 最差实践(bad-practice):开发者在方法里直接实例化线程池对象,然后...(应用gg了) 【HttpClient最差实践(bad-practice)】开发者在 http 工具方法中直接实例化 HttpClient,然后…… Crypto、Cipher与Password:Java加密开发的三个核心概念 知识VS技能:如何优雅判空? 20260604SR超时问题排查 推敲见文章:从 `try..catch` 看异常日志打印的正确姿势 从合同甲方是荒谬的“JD”谈起:软件开发不应遗忘的“常识” 别留小尾巴/尽快剪掉小尾巴:从一次“ABA”字段重命名,谈谈“解决问题要彻底” 常见的OOM错误 ( OutOfMemoryError全类型详解) 开发者暴露了一个无需授权访问的裸接口,我问:如果有人暴力请求怎么办? 【SQL性能优化篇】有了!治理慢SQL“WHERE create_time ORDER BY id”的良药---规避“Using filesort”性能杀手 高效查询商户日终余额:一个SQL的优化实践 Hutool 的 `TimedCache` 到期会自动清理吗? ——————hutool cache的"惰性清理"和"定期清理" Fastjson枚举反序列化:当字符串不是枚举常量名时,会发生什么? fastjson-EnumDeserializer类及源码分析 随笔20260309:我们都是围城里的人 `UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only` 异常解析 认识2个单词:goal/target —————— 为什么Maven是 "goal" 而不是 "target"? 聚合系统设计:策略模式(Strategy Pattern)在银行通道对接场景中的应用 这样构建对象,太帅了!—— 阶梯式Builder模式与代码整洁之道 注意!字段数据类型不匹配,这个sql会很慢 还在用ArrayList?用HashSet吧!--性能对比 分页查询还在用create_time去做降序?
#解决问题要彻底# 慢SQL治理完成后,如何防止同类问题“死灰复...
buguge · 2026-05-14 · via 博客园 - buguge

一、 问题现场:一个触发全表扫描的“企业名称”查询

今天巡检系统时,发现一条耗时超过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 被直接用作查询条件。

image

隐患即刻暴露:

  • 表规模巨大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>

这个简单动作的工程价值:

  1. 明确警示:任何后来者,在阅读或修改这段SQL时,都会首先看到这段醒目的警告,了解历史背景和性能禁忌。
  2. 防止误启用:如果需要临时测试或确有特殊场景,开发者必须主动删除注释。这个过程本身会促使他思考并评估风险,而不是无意识地复制粘贴一段“可用”的代码。
  3. 保留上下文:注释掉的代码本身,清晰地展示了“过去错误的样子”和“正确的字段是什么”(enterprise_id),提供了修改范本。
  4. 成本极低:这是一种几乎零成本、可立即实施的防护措施。

显然,这比约定规范或者互相周知,要更有效。毕竟,这东西并不能让团队人员都一一get。对于小规模团队或者需求研发高频迭代的团队,有妻如此。

五、 结语:解决问题要彻底

解决一个线上慢SQL,是技术能力的体现;而思考如何防止它再次发生,则是工程素养的彰显。

“解决问题要彻底”,不仅意味着修复眼前的Bug,更意味着通过代码注释、流程规范、知识传承、技术工具等多重手段,在系统中建立起防止同类错误复发的“免疫机制”。

从给危险的代码块加上一个醒目的警告注释开始,我们就是在为团队积累资产,为系统的长期稳定运行铺设基石。这一个小小的举动,体现的正是对工程质量负责、对后来者负责的工匠精神。

当然,注释警告是有效的临时“路标”。从工程体系上,我们还可以努力做得更多,以追求更彻底的解决:

  1. 知识沉淀:将此次优化案例写入团队Wiki或技术知识库,标题可为“Order表查询规范:禁止使用enterprise_name字段”。在新人入职或技术分享时进行传达。
  2. Code Review 强化:在团队代码审查清单中,加入针对大表、无索引字段查询的检查项。审查者遇到涉及核心大表的SQL变更时,应特别警惕。
  3. 数据库监控与告警:配置数据库监控,对长时间运行的SQL进行抓取和告警。一旦再次出现针对 order.enterprise_name 的慢查询,能第一时间发现并拦截。
  4. 推动根本重构:将“评估并从order表中移除冗余的enterprise_name字段”作为一个正式的技术债条目纳入规划。明确责任人,评估影响范围,制定最终清理计划。