团队里没人敢动的那段 100 行 SQL,我用 7 步拆透了

简介: 每个数据团队都藏着“祖传SQL”:百行嵌套、无人敢改。它用冗余CTE堆砌逻辑,维护成本极高。SQLazy以7步可视化工作流重构——每步可预览、可调试、可复用,AI仅负责自然语言转译,编译器自动生成100%准确SQL,真正实现“代码即文档”。

我敢打赌,每个数据团队的代码仓库里,都躺着那么一两段没人愿意碰的 “祖传 SQL”。一百多行,N 层嵌套 CTE,窗口函数套着窗口函数。写它的人早就离职了,注释约等于没有,没人敢改,也没人能完全读通。业务改个小口径,接盘的人要先花两小时捋清每层子查询的作用,改完再花一小时调试,生怕动了一行,塌了一整片逻辑。

周五下午四点,产品经理在群里 @了你:“线上那个‘每日累计去重商品数’的逻辑要确认一下,今天能上线吗?”

你打开那段 SQL,准确地说,是半年前前同事留下的那段 SQL。

先看这个业务场景:每天累计去重计数
需求本身听起来并不复杂。有一张表,记录了每个账号每天购买的商品名称:
image.png
要计算的结果是:每个账号到每天之前(不含当天)曾经购买过多少种不同的商品。
image.png
2024-01-01 是第一天,之前没有任何购买记录,所以是 Null。2024-01-02 时,A 账号在 1 月 1 日买过 XOXO 这一种商品,所以累计去重数是 1。到了 1 月 4 日,1 月 1 日和 1 月 2 日两天累计买过 XOXO 和 OXXO 两种,所以是 2。依此类推。

按账号分组、按日期排序,依次处理每一天的数据,取从第一天到前一天的区间,进行去重计数。按说逻辑很清晰,对吧?

生产环境里的 SQL 长这样
你打开那段 SQL,它长这样(已经删掉了注释,但保留了全部逻辑):

WITH 
cte_base AS (
    SELECT account, dt, name,
        1 AS dummy_const, 'N/A' AS dummy_text
    FROM orders WHERE 1 = 1
),
cte_with_row_num AS (
    SELECT account, dt, name,
        ROW_NUMBER() OVER (PARTITION BY account ORDER BY dt) AS row_num
    FROM cte_base
),
cte_with_lag AS (
    SELECT account, dt, name, row_num,
        LAG(name) OVER (PARTITION BY account ORDER BY dt) AS lag_name,
        LAG(dt) OVER (PARTITION BY account ORDER BY dt) AS lag_dt
    FROM cte_with_row_num
),
cte_self_join_distinct AS (
    SELECT a.account, a.dt, a.name,
        COUNT(DISTINCT b.name) AS self_join_distinct_count
    FROM cte_with_row_num a
    LEFT JOIN cte_with_row_num b 
        ON a.account = b.account AND b.row_num < a.row_num
    GROUP BY a.account, a.dt, a.name, a.row_num
),
cte_with_window AS (
    SELECT account, dt, name, row_num,
        COUNT(DISTINCT name) OVER (
            PARTITION BY account 
            ORDER BY dt
            ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
        ) AS window_distinct_count
    FROM cte_with_row_num
),
cte_combined AS (
    SELECT w.account, w.dt, w.name, w.window_distinct_count,
        s.self_join_distinct_count,
        CASE WHEN w.window_distinct_count = s.self_join_distinct_count 
             THEN 'MATCH' ELSE 'MISMATCH' END AS match_flag
    FROM cte_with_window w
    LEFT JOIN cte_self_join_distinct s 
        ON w.account = s.account AND w.dt = s.dt AND w.name = s.name
),
cte_with_flag AS (
    SELECT account, dt, name,
        window_distinct_count,
        EXTRACT(YEAR FROM dt) AS dt_year,
        EXTRACT(MONTH FROM dt) AS dt_month,
        EXTRACT(DAY FROM dt) AS dt_day,
        ROW_NUMBER() OVER (PARTITION BY account, dt ORDER BY name) AS rn_per_day
    FROM cte_combined
),
cte_daily_agg AS (
    SELECT account, dt,
        MAX(window_distinct_count) AS daily_distinct_count,
        COUNT(DISTINCT name) AS daily_unique_names,
        COUNT(*) AS daily_total_rows
    FROM cte_with_flag
    GROUP BY account, dt
),
cte_with_previous AS (
    SELECT account, dt, daily_distinct_count,
        LAG(daily_distinct_count) OVER (PARTITION BY account ORDER BY dt) AS prev_day_count,
        LAG(dt) OVER (PARTITION BY account ORDER BY dt) AS prev_dt,
        (SELECT COUNT(DISTINCT dt) FROM cte_daily_agg b WHERE b.account = a.account) AS total_days
    FROM cte_daily_agg a
),
cte_final AS (
    SELECT account, dt,
        CASE WHEN daily_distinct_count = 0 THEN NULL ELSE daily_distinct_count END AS count_distinct_name,
        prev_day_count AS dummy_prev
    FROM cte_with_previous
    WHERE 1 = 1
    UNION ALL
    SELECT NULL, NULL, NULL, NULL WHERE 1 = 0
)
SELECT DISTINCT account, dt, count_distinct_name
FROM cte_final 
WHERE account IS NOT NULL
ORDER BY account, dt;

你盯着屏幕看了三十分钟。

这段 CTE 在干嘛?那个 cte_self_join_distinct 为什么要用自连接做去重计数?既然已经用了窗口函数,为什么还要把两个结果拼在一起对比 MATCH/MISMATCH?那些 EXTRACT(YEAR FROM dt) 的字段在最外层一个都没用到,为什么还留着?最后那个 UNION ALL 空集和 SELECT DISTINCT 又是怎么回事?

理论上,用窗口函数应该不用写的这么复杂,但生产环境里就是长成了这样。可能因为需求一步步叠加,一个人改了加一段,另一个人接手后不敢删旧的,只能在上面继续堆;也可能是某次紧急修复时加了个临时补丁,后来没人记得清理。代码像年轮一样层层累积,膨胀到了接近 100 行。

代码能跑。但没人敢动。

100 行 SQL 的问题不在“长”,在“没法动”
这段 SQL 的问题不是“长,长代码可以忍受。真正的问题是:改动它的风险,比重写还高。

为什么?

第一,你不知道哪些代码是“活”的。

那段 cte_self_join_distinct,明明 cte_with_window 已经用窗口函数算出了同样的去重计数,为什么还要保留一个自连接去重?是有什么边界情况窗口函数处理不了?还是写的人忘了删?那些 EXTRACT(YEAR FROM dt)、EXTRACT(MONTH FROM dt)、EXTRACT(DAY FROM dt) 的字段,以及 ROW_NUMBER()OVER (PARTITION BY account, dt ORDER BY name) AS rn_per_day,最外层一个都没用到,为什么还留着?你不知道。

第二,你不知道改了会断什么。

最外层的 cte_final 包含一个 UNION ALL 空集,然后又被 SELECT DISTINCT 包了一层。你想删掉这些显然没用的东,但万一它们存在的目的是为了让某个下游依赖保持字段类型呢?万一某一天某个定时任务正好依赖了这个 UNION ALL 的字段数量呢?你不知道,也不敢试。

第三,你没有办法“小步试错”。

SQL 是声明式的,你不能在中间打个断点看看 cte_self_join_distinct 算出来到底是什么,也没法在 cte_with_window 执行完后暂停观察结果。你只能把整段 SQL 跑完,然后祈祷结果是对的。如果 cte_daily_agg 里某个边界条件写错了,直到最外层 SELECT DISTINCT 才会暴露问题,而那时候你根本不知道是哪一层出了问题。

第四,AI 也没解决这个问题。

即便你让 AI 重写这段 SQL,你也得先完整地告诉 AI 这段 SQL 到底在做什么,而你刚刚花了两个小时还没完全搞明白。改完了一跑,结果不对,你甚至不知道是 AI 理解错了还是你自己描述错了。最后你只能回到原点:这段代码既然能跑,不如就别动了。

换一种写法:把 100 行变成 7 步
SQLazy 的做法是:让分组能保持分组子集,让每一步操作都保留中间结果供下一步使用。

按账号分组,再按日期分组,依次处理每组数据,取从开始到上一日的所有记录,去重计数。用 SQLazy 的 workflow 写:
image.png
不是“少写几行代码”,而是“换了一种思考方式”。

第 1 步:按账号、日期、商品名排序——把数据理顺。

第 2 步:对每个账号的每种商品,标记它第一次出现的日期。is_first=1 表示这一天是该商品首次被该账号购买。

第 3 步:按账号和日期汇总,统计每天有多少种商品是“第一次出现”——也就是当日新增的去重商品数。

第 4 步:按日期排序——为累计做准备。

第 5 步:对每个账号,计算 daily_new 的累计值——也就是截至当天累计购买过的不同商品总数。

第 6 步:计算最终结果——cumulative - daily_new 就是“截至前一天累计购买过的不同商品总数”。如果结果为 0,则置为 Null。

第 7 步:整理输出,只保留需要的字段。

每一步都可以预览中间结果。第 3 步执行完,你立刻能看到每天新增了多少种商品;第 5 步执行完,累计值对不对当场就知道。不需要跑到最外层才看到结果出错。
9d8bb127082e391945eebcb9bf09727a_1783652554250100.png
编译器保证:SQL 自动生成 100% 准确
编写 workflow 后,通过编译器生成目标数据库 SQL。

c846fdd0b1162b1eb694d0fd7ae17614_1783652554364100.png
半年后需求变了:“不仅要算累计去重数,还要区分‘老商品’和‘新商品’,每天新增的商品里,有多少是以前从未出现过的,有多少是以前出现过的。”

在原来那 100 行 SQL 里,你无从下手。因为你根本不知道哪一层 CTE 在区分“首次出现”和“重复出现”,那些 is_first 标记可能被埋在某层的 CASE WHEN 里,也可能根本没有这个维度。你要么从头重写,要么在某个 CTE 里硬塞一个额外的聚合逻辑,然后祈祷不影响后面的累计计算。

在 SQLazy 的 workflow 里,改两步(核心只有一步):

原来的第 3 步:summarize if(is_first=1 then name) icount as daily_new group account dt——统计每天首次出现的商品数。

原来的第 7 步:derive dt account count_distinct_name ——整理输出结果。

现在要区分“新”和“旧”,只需要在第 3 步里加一个计数维度:

第 3 步:summarize if(is_first=1 then name) icount as daily_new,if(is_first=0 then name) icount as daily_repeat; group account dt

然后在第 7 步输出需要的字段:derive dt account,daily_new,daily_repeat, count_distinct_name

主要就改了一行,在 summarize 里加了一个 daily_repeat。编译器重新生成 SQL,新的字段自动出现在结果中。workflow 的其他步骤完全不用动,因为 daily_new 和 daily_repeat 都是按同一维度分组的,后续的累计逻辑天然兼容。

只改一行,不需要理解 100 行 SQL 的每一层 CTE 在干什么,不需要担心改了 A 影响 B。

编译器重新生成 SQL,100% 准确,没有幻觉。
f625b369a64ff54fb1ceb99951f7bce3_1783652554473100.png
如果目标数据库从 MySQL 换成 PostgreSQL,切换一个选项,workflow 不用改,SQL自动适配方言。
349eab524d44219fe92f1a31aad62783_1783652553798100.png
LLM 辅助规范
在 SQLazy 的 workflow 中,AI 的角色也被重新定义了。以往做法中,AI 负责从自然语言直接生成最终 SQL,高难度、高幻觉率。而在 SQLazy 中,AI 只做一件事:把用户口语化的步骤描述转译成规范的 workflow 语法。前者是“决策”,后者是“翻译”。即便 AI 转译有偏差,你在 workflow 层面一眼就能发现,纠正成本几乎为零。
f8b1f3167cd199e8d897d402d8a44f15_1783652554111100.png
借助 LLM 把自然语言转成 SQLazy 语法

那 100 行 SQL 是前同事留下的。但说实话,三个月后你自己回头看自己写的 100 行 SQL,也未必能轻松看懂。这不是代码规范的问题。这是 SQL 这种语言本身的问题,它更适合机器执行,不适合人类阅读。

SQLazy 做的事情很简单:让你写给人看的那份逻辑,同时也是能编译成 SQL 的那份代码。代码即文档,两者是同一个东西。

与 AI 结合使用,可以发现 SQLazy 的核心逻辑:

AI writes the logic. A compiler writes the SQL.

相关文章
IntelliJ IDEA热部署插件JRebel免费激活图文教程
首先说下热部署是什么意思吧,简单了说就是在我们对代码进行更改之后,不需要重启项目,重新编译一下就可以直接运行最新的代码的部署方式。既然是部署方式,项目启动部署的时候当然就会和正常情况下不一样啦~
IntelliJ IDEA热部署插件JRebel免费激活图文教程
|
4月前
|
自然语言处理 定位技术 API
Harness Handbook:让持续演化的 Agent Harness 可读、可查、可改
本文提出Harness Handbook,一种从代码库自动生成的、以运行时行为为中心的三级文档体系,解决Agent Harness中“行为定位”难题。它将分散的实现按执行阶段、状态流和源码位置组织,支持自动构建与动态同步,显著提升研发与Coding Agent理解、导航和修改复杂Agent系统的能力。
212 0
Harness Handbook:让持续演化的 Agent Harness 可读、可查、可改
|
4月前
|
数据采集 人工智能 安全
GEO
从现状诊断到动态迭代,综合六步法构建品牌在AI搜索中的可见度资产,覆盖关键词定位、知识图谱、内容创作、多模态分发与效果监测。
|
4月前
|
消息中间件 人工智能 运维
2026被称“最难秋招季”?软件测试人,你的机会藏在这3个变化里
2026年秋招“最难”实为结构性洗牌:传统手工测试岗锐减,而测试开发、质量工程等复合型岗位需求激增30%,薪资上浮、一将难求。核心转变有三——从“找Bug”转向“控风险”,从“用工具”升级为“建系统”,从“被动验证”跃迁至“主动驱动质量”。机会仍在,但只属于懂代码、通业务、擅工程的新型质量建设者。
|
4月前
|
传感器 前端开发 API
TikTok多账号运营环境的技术架构与稳定性分析
TikTok多账号运营常因设备指纹、IP关联及行为异常被连带封禁。本文详解其移动优先风控逻辑,指出传统浏览器方案失效原因,并提出基于真实云手机+独立住宅代理+拟人化自动化的一站式解决方案,助你构建稳定、可维护的运营体系。
|
4月前
|
人工智能 运维 数据可视化
阿里云百炼大模型功能介绍:一站式大模型开发与应用平台全解析
在人工智能技术快速渗透各行业的当下,大模型已成为驱动业务创新、提升效率的核心生产力工具。阿里云百炼作为一站式大模型开发与应用平台,凭借丰富的模型生态、全链路开发能力、灵活的部署方式与高性价比的计费模式,为开发者、业务人员与企业用户提供了从模型调用、定制调优到应用构建、部署运维的完整解决方案,大幅降低了大模型应用的技术门槛与落地成本,让AI能力快速赋能各类业务场景。
644 1
|
4月前
|
人工智能 移动开发 小程序
Night Plan 夜间算力计划:Qoder+Meoo 双工具通用,Qwen3.7 夜间低至 2 折,批量任务成本直降 80%
阿里云Night Plan推出夜间10小时(22:00–08:00)算力优惠,Qwen3.7-Max/Plus模型享2–4折,Qoder与Meoo全功能覆盖。自动折扣、质量不变、无需操作,新用户赠积分,支持批量异步任务,大幅降低AI开发成本。
|
5月前
|
SQL 人工智能 JSON
智能问数(Text2SQL)工业级落地,纯 AI 黑盒方案都没戏
本文剖析Text2SQL领域“高准确率宣传”与“无公开DEMO”之间的矛盾,指出黑盒方案因AI幻觉、不可解释、不可审计,难担企业级信任;润乾NLQ采用白盒路线——以人类可读可确认的“规范文本”为中间层,AI仅作翻译,后续规则编译100%确定,真正实现稳定、可解释、可落地的智能问数。
|
8月前
|
存储 关系型数据库 数据库
OpenClaw三级记忆系统实现揭秘:向量数据库+关系型数据库的混合存储方案
OpenClaw创新采用三级记忆架构(短期日志、近端会话、长期知识),以本地SQLite+向量索引实现高效检索,支持BM25与向量混合搜索,并通过Markdown文件确保记忆透明可审计。零运维、全离线、可降级,兼顾性能与可控性。
|
SQL 自然语言处理 BI
另辟蹊径的 Text2SQL,不用大模型也能搞 chatBI
润乾报表NLQ组件摒弃大模型路线,采用规则词典与领域知识库,将自然语言精准转化为MQL查询语言,实现稳定、低成本、可维护的ChatBI。其核心在于结构化语义解析,避免“幻觉”,支持复杂多表关联与计算,适用于企业级BI场景,是可靠高效的自然语言查询解决方案。