AI 时代,代码即文档的 SQL 编写方案

一段“能跑但没法改”的 SQL

先看这段生产环境里的 SQL,目标是“合并每个账户内所有重叠的时间区间”:

SELECT account_id, MIN(start_date) AS start_date, MAX(end_date) AS end_date
FROM (
  SELECT account_id, start_date, end_date, prev_max,
    SUM(CASE WHEN start_date > prev_max OR prev_max IS NULL THEN 1 ELSE 0 END) 
      OVER (PARTITION BY account_id ORDER BY start_date) AS gid
  FROM (
    SELECT account_id, start_date, end_date,
      MAX(end_date) OVER (
        PARTITION BY account_id 
        ORDER BY CASE WHEN account_id IS NOT NULL THEN 1 END, start_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
      ) AS prev_max
    FROM acc
  ) t1
) t2
GROUP BY account_id, gid

这段 SQL 用到了窗口函数 MAX()OVER 和 SUM() OVER 的累加技巧,还用了 ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING 这种大多数人要查文档才能确认的写法。语法完全正确,能跑出结果。

但半年后需求变了,比如“合并条件从‘重叠’改成‘间隔不超过 3 天也算合并’”,改起来难度极大,基本等于重写。因为你得从头理解这段 SQL 的逻辑,在脑子里重新执行一遍窗口函数的计算过程,才能确定改哪里、怎么改。代码能跑,但看懂它的代价,和重新写一遍差不多。

这就是 SQL 开发的常态:代码是写给机器执行的,不是写给人类阅读的。

为什么 SQL 的“文档化”特别难

SQL 是声明式语言,告诉数据库“要什么”,而不是“怎么做”。这带来的问题是:业务逻辑被编码在语法结构里,而不是显式表达出来。

  • 一个窗口函数 LAG(amount,1) OVER(PARTITION BY customer_id ORDER BY month) 背后可能对应着“计算上个月交易额”这个业务概念

  • 一段 SUM(CASE WHEN ... THEN 1 ELSE 0 END) OVER (...) 的累加技巧,背后可能对应着“按条件分段”这个逻辑

  • 但这些业务语义在 SQL 中都是隐含的,读者需要自己去推断每一段语法“到底在干什么”

更糟的是,注释解决不了这个问题。代码改了注释不一定会改,久而久之,注释比代码更不可信。而且 SQL 的嵌套结构天然抗拒注释,注释插在多层括号之间,readability 只会更差。

AI 编程也解决不了这个问题。用 AI 生成了一段 SQL,跑通了,提交了。但不会把 AI 的提示词留在代码仓库里,留下的只有最终 SQL。下次需求变了,还是得面对同一段 SQL,重新理解它的逻辑。即便把当初给 AI 写的 spec 也留下了,AI 也不能保证再次生成同样的 SQL(LLM 是不确定的)。改了 spec 让 AI 重新生成,生成的 SQL 可能完全不同,还得重新审计一遍,相当于重写,而不是“修改”。

所以,AI 编程并没有改变“文档与代码脱节”这个根本问题:

除非,把 spec 和 SQL 做成一体的,让它俩永远配对,改 spec 就自动改 SQL。

“代码即文档”是个理想, SQL 让这个理想变得更加遥不可及。

这里要澄清一个常见的误解:“代码即文档”不等于“在代码里写很多注释”。注释是辅助性的,真正的“代码即文档”意味着三件事:

  1. 代码的结构本身就表达了逻辑——看代码就像看文档一样清晰,不需要额外解释

  2. 代码和文档是同一份东西——不存在“代码改了文档没改”的问题

  3. 新人接手时,读代码就能理解业务——不需要额外培训,不需要前任留下口头交代

在 SQL 领域,这意味着 SQL 本身应该具备自描述性,每一句代码都在清晰地告诉读者“这一步在做什么”,而不是“这一段语法实现了什么”。但原生 SQL 做不到这一点,多层嵌套随处可见,可读性极差。

所以解法是:让 spec 同时可以阅读和执行,spec 即文档、spec 即代码。

SQLazy 的解法:workflow 即文档

SQLazy 的做法是:不直接写 SQL,而是用 workflow 描述逻辑步骤。每个步骤就是一个原子操作,步骤的组合就是完整的业务逻辑。

还是上面那个“合并重叠区间”的例子。用 SQLazy 的 workflow 写:

步骤

操作和含义

1

sort account_id, start_date

2

compute end_date[:-1] max as prev_max

3

segment condition start_date > prev_max as gid

4

summarize start_date min, end_date max

这几行 workflow 几乎可以当自然语言来读:排序、计算之前最大的结束日期、按条件分段、汇总取最早和最晚。大部分操作不用专门学习就能猜出含义,只有 segment 稍微需要解释,意思是 "按条件把数据分成不同的组",但结合后面的可视化分步执行,看一眼中间结果就立刻明白了。

..

在线运行本例:https://www.sqlazy.com/?4Bs

这份可编译的 workflow 本身同时也是文档

  • 每一步都有明确的业务含义。sort 就是排序,compute 就是计算辅助值,segment 就是按条件分组,summarize 就是汇总

  • 步骤的顺序就是思考的顺序。先排序、再计算、再分段、最后汇总,和你手工处理这个问题的思路完全一致

  • 不需要额外的注释来解释“这段 SQL 在干什么”。workflow 的每一行已经说清楚了

  • 半年后需求变了,打开 workflow 直接改对应步骤。

代码和文档是一份东西,不是两份。这才是“代码即文档”的真正落地。

再举一个例子:按账户分组,活动间隔超过 1 小时则重置序列号。用 SQLazy 的 workflow 写:

Value

Anchor

Statement

t2

numEvents

sort account_number asc, dt asc

t3


segment condition ((dt[-1] elapse 3600 second)<= dt) partition account_number as grp



compute # as seq partition account_number, grp

三行 workflow,业务逻辑一目了然:“超过 1 小时就重新编号”。对应的 SQL 如果手写,至少需要嵌套两层窗口函数,还要处理 LAG 的时间差计算。但有了 workflow,你不需要关心 SQL 怎么写,编译器替你搞定。

编译器保证:文档和代码永不脱节

传统模式下,文档和代码分离,靠人工同步,而人工同步很容易出错。文档写完了,代码改了没同步;或者代码改完了,文档忘了更新。

SQLazy 的模式是:workflow(文档)→ 编译器 → SQL(代码)

前面的 workflow 编译成 SQL(可切换不同数据库):

..

文档是源头,代码是编译产物。只要 workflow 不变,生成的 SQL 就不会变;workflow 改了,SQL 自动重新生成。不存在“文档改了代码没改”或“代码改了文档没改”的问题,因为它们是一体两面。

更换数据库时,只需切换目标选项,从 MySQL 换成 Oracle,从 PostgreSQL 换成 Snowflake,编译器自动生成对应方言的 SQL。workflow 不用改,文档不用重写。

“代码即文档”在 SQL 领域一直是个难以实现的理想。没有 SQLazy 这种 workflow 机制,代码和文档永远是两个东西。你写了 SQL,还得另外写文档解释它在干什么;文档和 SQL 之间的同步,只能靠人工,而人工同步就会出错。

SQLazy 改变了这件事:把复杂的 SQL 拆成清晰的步骤,步骤本身就是文档,编译器保证代码与文档永远一致。

这不是在 SQL 外面包一层壳,而是改变了 SQL 的开发范式:从“写一段机器读的代码”变成“写一套人读的逻辑,让机器去翻译”。workflow 是给人看的,SQL 是给数据库跑的,两者由编译器保证一致,不需要人工同步。

下次你打开一个三个月前写的 SQL 文件,发现自己完全看不懂的时候,不妨想想:如果当初写的是 workflow,现在还会这么痛苦吗?