
接手过一个跑了八年的报表系统里面的 SQL 是我见过最壮观的东西。单条语句一千两百行FROM 子句里嵌着四层子查询同一个订单表被 JOIN 了七次没有一行注释。你问当初写这段代码的人为什么要这么写他早离职了。这种 SQL 就是典型的历史复杂 SQL业务不敢删DBA 不敢动新来的开发看了一眼就申请转岗。我们最后找到一条务实的路子用 CodeBuddy 做语义理解用 SQLazy 做结构化解析和等价改写把几十条没人敢碰的历史 SQL 逐步拆成了可维护、可优化、可交接的版本。整条流程跑下来最大的感受是盘活历史 SQL难点根本不在 SQL 本身而在怎么让机器替你读懂“人话”再用确定性手段验证改写的正确性。这篇文章把这条链路完整复盘一遍适合还在跟历史 SQL 搏斗的后端、DBA、数据分析师。1. 这个项目到底在解决什么问题1.1 历史复杂 SQL 的真面目所谓“历史复杂 SQL”不是那种几十行、能一眼看懂的业务查询。它通常长这样一条 SQL 里叠着多层子查询子查询里又套子查询同一个表为了拿到不同维度的数据被反复 JOINWHERE 条件里塞了一长串 OR聚合逻辑分散在多个派生表里还有一堆DECODE、CASE WHEN、NVL之类的函数嵌套。更要命的是这些 SQL 往往经历了多年迭代。最初写的人只完成了 60%后面的人为了修 bug 补一层为了加渠道再套一层为了兼容脏数据又包一层。每一层都有当时的合理性但没有人更新注释。到了我接手的时候代码仓库里那一百多条核心 SQL基本处于“能跑但是没人敢碰”的状态。这类 SQL 带来的问题非常具体。业务提需求开发想改一个过滤条件但看不出这个条件会被哪层子查询引用改了怕影响其他分支。数据库要做版本升级DBA 想并行优化但一条 SQL 里子查询太多执行计划根本读不懂。新人接手时想靠断点调试但 SQL 不是过程式代码跑一遍只能看到一个结果看不到中间状态。1.2 为什么会成为“不敢碰”的资产说到底大家不碰历史 SQL 不是因为懒而是因为风险不可控。没有文档。业务逻辑全藏在 SQL 的结构里没有人能用一句话说清楚“这个查询到底在算什么”。没有测试。系统上线早当时没有自动化回归环境现在想补测试又不知道该以哪个版本的结果为基准。没有责任边界。这些 SQL 关联财务、订单、库存等核心数据改错一行轻则报表对不上重则线上事故。这种情况下最优解当然是“别动它”。但这种静态平衡维持不了多久。业务要迭代数据量在涨数据库要升级团队要交接每一件事都会撞上这些历史 SQL。我参与的项目就是典型的导火索核心库要从旧版本升级到新架构迁移前必须先把 SQL 里的隐患排掉同时业务方提了一批新报表需求需要复用这些老查询里的指标口径。所以“盘活”的定义不是推翻重写而是在不改变业务语义的前提下让这些 SQL 重新具备三个属性可读、可优化、可交接。可读是指人能看明白可优化是指性能瓶颈能被定位可交接是指新人来了能接手老同事走了系统不至于变黑盒。2. 方案选型与工具分工CodeBuddy 和 SQLazy 谁干什么2.1 为什么纯 AI 或者纯规则解析都搞不定一开始我们也想过两条路。一条是纯靠 AI 编程助手下场把 SQL 全扔给大模型让它解释、改写、优化。另一条是走传统静态分析用 SQL 解析工具做格式化、查重、检测语法错误。两条路单独走都走不通。纯 AI 的问题在于它擅长语义理解但不擅长确定性改造。你问它“这段 SQL 在算什么”它能说出个七七八八你让它“把这条 SQL 改写成等价且更快的版本”它可能生成一段看起来合理、实际上改了业务口径的代码。大模型对 SQL 方言细节、NULL 处理、边界分支的理解并不稳定尤其面对上千行的嵌套查询时很容易出现幻觉——所谓“幻觉”就是它自信地认为LEFT JOIN和INNER JOIN在这里等价或者把COUNT(*)和COUNT(字段)混为一谈。纯规则工具的问题相反。它能精确解析语法树能格式化、能查重、能发现“这个 JOIN 从未被使用”但它不理解业务。SQL 里一堆结构相似的子查询到底哪个是核心逻辑哪个只是历史遗留的冗余规则工具分不清。更关键的是等价改写这件事需要判断“改完后的结果集和原来是否一致”这必须靠语义模型支撑纯规则难以保证。所以我们的选择不是二选一而是让两者互补CodeBuddy 负责读SQLazy 负责改人负责审。2.2 CodeBuddy语义理解的大脑CodeBuddy 是腾讯云的 AI 编程助手日常在 IDE 里就能用。把它从“写代码”的场景搬到“读 SQL”的场景其实非常顺手。在整个项目里CodeBuddy 的核心职责不是生成新代码而是充当一个带着二十年经验的 DBA 顾问。我把历史 SQL 贴给它让它做四件事用自然语言解释这段 SQL 的业务语义识别每一层子查询在整体逻辑中扮演的角色标注可能影响性能的写法给出等价改写建议并说明依据。这里有个小技巧不要一上来就让它“优化 SQL”而是要把它当成一个不懂业务的分析师先逼它把逻辑讲清楚。我会用固定的提问模板你是资深 DBA。下面这段 SQL 来自订单报表模块请先不要给出任何修改建议只用自然语言描述这段 SQL 的完整逻辑它从哪些表取数经过哪些过滤条件在什么粒度上聚合输出哪些字段各字段的业务含义可能是什么。遇到看不懂的写法明确说“这里我看不懂”。等它把语义讲透我再追问第二层基于刚才的理解请识别这段 SQL 中可能导致性能问题的写法例如隐式类型转换、函数作用于索引列、冗余 JOIN、不必要的关联子查询、可合并的派生表。逐条说明为什么可能是问题并标注风险等级。得到这些信息后CodeBuddy 的角色就从“解释者”变成了“改写顾问”。但注意它给出的 SQL 永远只是参考我不会直接把那段代码拿去上线。AI 的改写建议负责打开思路最终的等价性判断要靠 SQLazy 和人工验证把住。2.3 SQLazy结构解析与等价改写的手SQLazy 是我们引入的 SQL 静态解析与等价改写专项工具。它的工作方式和 AI 完全不同核心能力有三块。第一解析。SQLazy 能把 SQL 拆成抽象语法树兼容 MySQL、PostgreSQL、SQL Server、Oracle、DB2 等主流方言能识别出每个关键字、表名、列名、子查询之间的结构关系。这比人眼靠谱得多一条一千行的 SQL靠肉眼找配对括号都能疯掉工具几毫秒就能输出完整结构。第二清洗。格式化、去重、消除冗余 JOIN、合并重复子查询这些机械性工作全部交给它。比如一段 SQL 里两个子查询长得几乎一样只是过滤条件差了一个值SQLazy 能自动识别并提示“这两个分支可以合并”。第三等价改写。这是最值钱的部分。SQLazy 内置了一套改写规则库比如把OR改写成UNION ALL、把关联子查询改写成JOIN、把多层嵌套改写成 CTE、把IN子查询改写成EXISTS。与规则配套的还有差异比对能力改完以后它能分别对原 SQL 和新 SQL 做语义分析指出哪些列的操作方式发生了变化从语法树层面告诉你“这两个查询结构是否等价”。如果你手头没有 SQLazy 这个工具用 SQLFluff 加 sqlparse 也能搭一个接近的体系但规则库和比对能力需要自己攒。我的建议是核心目标不是某个具体工具而是“用确定性手段验证 AI 的猜测”。工具叫什么名字不重要重要的是流程里必须有这一步。2.4 两者协作的完整流水线在实际操作中CodeBuddy 和 SQLazy 并不是并行使用而是一条环环相扣的流水线。先用 CodeBuddy 读懂 SQL输出业务语义描述然后把这份描述交给业务方确认判断 AI 的理解是否符合真实业务。确认完后把原 SQL 丢给 SQLazy 做格式化、结构分析和等价改写生成新版本。新版本再交给 CodeBuddy 做一次 review让它对比新旧差异看看有没有语义漂移的迹象。最后SQLazy 做语法树比对测试库做结果集回归全部通过才允许进入人工评审。整个链路里人始终在关键节点把关。CodeBuddy 的产出是“理解”SQLazy 的产出是“确定性变换”两者加起来的价值就是既有了 AI 对复杂逻辑的泛化能力又有了规则系统对改写过程的强约束。3. 实操全流程从烂 SQL 到可维护资产3.1 第一步盘点 SQL 资产与风险分级动工之前先得知道家底有多少。我们从三条线收集 SQL数据库慢查询日志、应用侧的 SQL 日志、代码仓库里手写的 SQL 片段。慢查询日志能筛出“跑得慢”的 SQL是最直接的线索。应用日志能看到调用频率和响应时间。代码仓库里的 SQL 则是全面的但不一定都上线了。三条线交叉比对后我们会得到一份完整的 SQL 清单。拿到清单别急着改先分级。分级标准不只看执行耗时还要看三个因素SQL 的复杂度、涉及业务的重要程度、改动后影响范围有多大。我们当时分成三档风险等级判定特征处理策略A 级单条超过 500 行涉及财务、订单、库存等核心指标改动后影响多张报表单独排期逐条攻坚必须走完整回归B 级100 到 500 行涉及常规业务查询影响范围可控按模块批量处理每个模块做一次回归C 级100 行以内逻辑清晰能直接看懂顺手格式化、加注释不做大改A 级 SQL 看起来吓人但数量往往不多。我们当时压舱石级别的问题 SQL 一共就十四条但它们贡献了系统里将近一半的慢查询和绝大多数事故风险。把精力集中在 A 级上性价比最高。3.2 第二步让 CodeBuddy 读懂每一段历史逻辑拿到一条 A 级 SQL我不会直接贴给 CodeBuddy 就开始对话。先做预处理把 SQL 里的硬编码数值脱敏把生产环境的表名前缀统一成测试环境的命名规则。这一步必须做原因后面细说。脱敏之后才开始人机协作式解读。我习惯把解读过程分成三个回合。第一回合让 CodeBuddy 完整解释。它输出的描述通常能覆盖七八成逻辑但中间会夹杂一些含糊表述比如“这里似乎用于过滤已取消的订单”这种“似乎”就是我需要进一步确认的信号。第二回合针对含糊点定向追问。我会挑出它没讲清楚的部分比如某一层子查询为什么要关联两次同一张表某个LEFT JOIN实际目的到底是什么。CodeBuddy 会根据上下文重新推理有时候能给出合理的解释有时候会承认自己也不确定。承认不确定反而是好事说明这个位置确实存在理解盲区。第三回合让 CodeBuddy 生成一份简洁的“逻辑拆解说明”按子查询层级编号逐段标注业务含义再配合表格类输出列出每个字段可能的含义。这份说明会直接发给业务方确认。业务方看完往往能纠正不少理解偏差——比如某个字段在他们业务里根本不是什么“订单状态”而是“结算方式标识”。这一步做完SQL 才算是真正被读懂了。3.3 第三步用 SQLazy 做结构化清洗与等价改写语义确认后进入 SQLazy 的重写环节。我的原则是先清洗再重写最后才是优化。这三个阶段不能打乱。清洗阶段SQLazy 会把 SQL 格式化成统一风格规范缩进和对齐标注出每个子查询和 JOIN 的层级关系。做完这一步很多表面复杂的问题就暴露了有的子查询根本未被引用有的 JOIN 条件写错了导致笛卡尔积有的重复片段完全一样。重写阶段按语义块逐个处理。每处理一个语义块就生成一个独立版本并让 SQLazy 输出新旧差异说明。拿一段典型 SQL 举例原始的写法是这样的SELECT a.id, a.name, ( SELECT SUM(o.amount) FROM orders o WHERE o.user_id a.id ) AS total_amount FROM users a WHERE a.created_at 2023-01-01;这段里有个关联子查询。在数据量小的时候它没问题但一旦用户量大每返回一行就要执行一次子查询性能就会很差。SQLazy 会把它改写成LEFT JOIN加分组的形式SELECT a.id, a.name, COALESCE(SUM(o.amount), 0) AS total_amount FROM users a LEFT JOIN orders o ON o.user_id a.id WHERE a.created_at 2023-01-01 GROUP BY a.id, a.name;这里有个必须注意的细节原 SQL 里没有订单的用户子查询返回NULL而LEFT JOIN加分组后同样会得到NULL所以行为一致。但如果我们用的是INNER JOIN没有订单的用户会被直接过滤掉结果集就变了。这就是我反复强调“语义稳定性”的原因SQLazy 在给出改写建议时会标注这类差异但最终仍然需要人来确认它不会替你懂业务。再比如OR改UNION ALL。WHERE status ACTIVE OR status PENDING这种条件在部分数据库优化器下可能走不好索引。拆成两个查询再UNION ALL往往能各自利用索引性能提升明显。但要小心UNION会去重而UNION ALL不去重如果原查询本应保留重复行贸然改成UNION就又引入了一个新 bug。常见改写模式我整理了一张表方便对照原始模式改写方向收益主要风险关联子查询LEFT JOIN 聚合减少逐行子查询开销聚合粒度变化导致结果集不一致OR 条件UNION ALL各分支可独立走索引去重语义变化重复行丢失IN 子查询EXISTS / JOIN避免大表全扫描关联条件不正确导致结果扩大或缩小多层嵌套子查询CTE 拆分结构清晰便于定位问题部分数据库会物化 CTE造成额外开销同一表多次 JOIN条件合并或预聚合减少表扫描次数预聚合粒度错误导致数据重复优化阶段放在最后。这个阶段 SQLazy 能提供执行计划比较但真正的性能验证还是得靠测试库实跑。所以我通常把优化和回归验证放在同一步。3.4 第四步数据对比与执行计划双重回归改写完成后进入唯一能证明“我改对了”的环节回归验证。没有这一步所有等价性讨论都是空谈。先做结果集对比。在测试库里对原 SQL 和新 SQL 分别执行同样的查询然后比较结果集。最实用的方式是给每一行计算一个校验哈希值对查询出的关键字段做拼接再用哈希函数生成一个指纹然后把新旧结果的指纹按照主键排序后全量比对。两边哈希完全一致说明结果集大概率相同。但“大概率”还不够我会额外抓边界场景。业务上最容易出问题的是空值、零值、重复数据、极端日期这几类。比如有一张订单表里存在订单金额为 0 的记录有的历史 SQL 在用SUM时不会受影响但改成COUNT(字段)后这些记录可能导致计数不一致。这类边界用例需要在回归用例里显式覆盖。再执行对比。SQLazy 可以同时拉出新旧两版的执行计划并标出 Cost 差异、扫描方式和预估行数。如果新计划的扫描行数明显降低说明改写确实让查询路径变短了如果成本反而上升就需要回看改写方式是否合理。这里有一个容易被忽略的点执行计划反映的是优化器对当前统计信息的判断不代表真实运行时表现。最终结论永远以测试库实跑的耗时和资源消耗为准。我们的流程是先在数据量较小的情况下验证逻辑正确性再导出一份接近生产规模的数据跑全量记录耗时、逻辑读、CPU 消耗等指标。只有所有指标都不差于原 SQL才算完成一个语义块的改写。3.5 第五步人工评审与知识沉淀改完不等于盘活。如果改完的 SQL 还是一堆看得懂但没人负责的代码三个月后又是下一个历史包袱。所以最后一步是人工评审和知识沉淀。每完成一条 A 级 SQL我们都会组织一个小型评审会参与的人包括改动者、核心业务方代表、至少一位不熟悉这段 SQL 的后端同事。评审的标准很简单让那位不熟悉的同事看着新 SQL 和 CodeBuddy 生成的逻辑说明复述一遍它的业务逻辑。如果他能在十分钟内讲清楚主线就说明这次改造是成功的。评审通过后把所有辅助资料整理进知识库CodeBuddy 的逻辑拆解说明、SQLazy 的改写差异报告、回归测试结果、评审记录。这些文档的价值不在当下而在于半年后业务方说“这个口径要微调一下”的时候新人能像读产品文档一样快速定位到应该改哪一行。为了让知识真正沉淀下来我们还做了一件事把高频改写模式整理成内部 SQL 编码规范。比如“禁止在大表上写关联子查询”“禁止在索引列上加函数”“禁止为了凑结果用 UNION 替代 UNION ALL”。这些规范不是拍脑袋定的全都来自这次盘活过程中的真实踩坑。4. 常见问题与排查技巧实录4.1 改写后结果不一致怎么查这是盘活过程中最经常炸的问题SQLazy 跑完重写逻辑检查看起来没问题结果集一对比差了几百行。遇到这种情况第一反应不要怀疑 SQLazy 的能力而是按下面的顺序排查。先看 NULL 处理。子查询改 JOIN 后COUNT(字段)会因为 NULL 被忽略而变化SUM则可能因为关联出多行而翻倍。检查关联字段是否有 NULL关联后是否产生重复行这是第一个排查点。再看隐式类型转换。数据库的隐式转换有时候会改变过滤顺序。比如字符串字段和数字字段直接比较优化器会统一转成一种类型改写成 JOIN 后关联键的排序规则可能发生变化导致范围判断结果不一样。还有浮点精度。钱的字段如果在数据库里存成浮点SUM后直接比较很危险常规做法是用类型转换或者ROUND到一个合理的精度后再比较。排查手段上最笨但最有效的办法是二分定位。把新旧 SQL 各自拆成中间结果原 SQL 拆成“外层查询 第一层子查询”新 SQL 也拆成对应的两段先比较子查询的输出再比较外层输出。哪一层开始出现差异问题就在哪一层。这个过程中 CodeBuddy 可以辅助分析差异点的 SQL 片段SQLazy 能输出语法树的 AST diff两者叠加通常能快速锁定问题行。4.2 方言兼容与动态 SQL 的应对历史系统里 SQL 方言混杂是常态。有人拿 SQL Server 的GETDATE、ISNULL、TOP写法有人用 MySQL 的LIMITDB2 里还有一堆判断字段是否为数字的函数。SQLazy 解析的时候需要显式指定方言否则语法树会解析失败。我们在处理跨库 SQL 时会先用 CodeBuddy 做方言转换再用 SQLazy 做结构校验。更头疼的是动态 SQL。存储过程里拼出来的查询SQLazy 根本看不到全貌因为语句在执行前是字符串拼接后的结果。我们的处理方式是让 CodeBuddy 分析动态拼接的模板基于各种参数组合生成几个典型样本再对样本分别做解析和改写。这个过程无法做到 100% 全覆盖但覆盖了线上实际调用频率最高的路径已经能解决大部分问题。另外提醒一句别忽略表结构漂移。我们遇到过一次no such column错误SQLazy 解析正常但跑起来就说找不到字段。排查后发现是测试库和开发库的表结构没同步字段在某个版本被重命名了。任何与 SQL 有关的排查第一步先确认环境里的表结构完全一致经验之谈能省下很多冤枉时间。4.3 性能不升反降怎么办改写后性能没变好甚至变差这种情况不少见。最典型的坑有三个。第一个坑是 CTE 被物化。SQL 里用 CTE 是为了可读性但某些数据库会把 CTE 结果物化成临时表如果这个 CTE 本身数据量很大、又只用到很少一部分字段性能反而不如原查询。遇到这种情况要么把 CTE 也改写成子查询要么在 SQLazy 里开启“CTE 内联”选项。第二个坑是窗口函数排序开销。窗口函数看起来优雅但ROW_NUMBER()或LAG()底层通常要排序。如果参与排序的字段没有索引排序的代价会远超原关联子查询的开销。改写前先确认排序字段有没有被索引覆盖。第三个坑是 OR 改 UNION ALL 后扫描次数增加。原写法虽然可能用不上索引但只扫一次表拆成 UNION ALL 后每条分支各扫一次如果表太大总 IO 反而更高。所以改写优化不是机械套规则一定要结合数据量和实际执行计划判断。我的做法是每个语义块改写完成后都做一次旧新 SQL 的背靠背压测至少在三种数据形态下测试小数据集、正常规模、接近生产的大规模。改完性能没有正向收益的直接回退宁可保持原样也不为了风格上的“优雅”牺牲稳定性。4.4 安全合规与数据脱敏最后聊一个容易被忽略的问题安全合规。把生产环境的核心查询丢给 AI 工具分析时代码或注释里可能带着敏感字段名、业务规则甚至审计字段。这不仅是数据泄露风险在合规层面也很敏感。我们的做法是在贴给 CodeBuddy 之前统一做一个脱敏处理替换真实表名为代号抹掉具体客户编号和时间戳只保留查询结构。SQLazy 因为是本地运行的解析工具没有数据外发风险但它的日志和差异报告同样会被保存到知识库也需要控制访问权限。另外凡是涉及财务、订单这类核心数据的 SQL即使改完了也不能一个人直接提交。必须保留完整的“原 SQL 新 SQL 差异说明 回归结果”四件套提交到代码仓库时同步挂上评审记录。这样做的意义在于万一上线后出了问题我们能迅速知道哪次改动引入了问题也能立即回滚到上一个版本。5. 写在最后几点实在的体会这套 CodeBuddy 加 SQLazy 的组合跑下来我们用了大约六周处理了四十多条核心 SQL其中十几条是 A 级钉子户。最大的体会是别一上来就想把整条 SQL 改成“教科书风格”先把语义搞清楚再谈优化。盘活历史 SQL 不是为了炫技是为了让后面接手的人少掉头发。如果只让我说一个最实用的小技巧那就是每次只改一个语义块保存一个可回滚版本回归对比通过了再动下一块。修改不在多而在于每一步的结论都站得住脚。慢就是快。