SQLazy:分组累计求和

问题描述:只保留开票行,金额累计从上次开票开始

有一张业务流水表(包含 ID、Date、Invoiced 和 Amount 四个字段)。每个月有一条记录,其中 Invoiced=1 表示该月开了发票。

现在要输出:每个 ID 下所有开票月份 ,并且每张发票的金额等于自上个月开票(或开始)到当前月所有金额的总和(包括当前发票本身)。

源数据:

ID Date Invoiced Amount
AAA 2023-01 0 10
AAA 2023-02 0 15
AAA 2023-03 1 15
AAA 2023-04 0 10
AAA 2023-05 0 10
AAA 2023-06 1 10
BBB 2022-05 0 40
BBB 2022-06 1 20
BBB 2022-07 0 30
BBB 2022-08 1 30

期望结果:只保留开票行,每行 Amount 是自上次开票以来的累计值。

ID Date Invoiced Amount
AAA 2023-03 1 40
AAA 2023-06 1 30
BBB 2022-06 1 60
BBB 2022-08 1 60

以 AAA 为例:

  • 第一张发票 2023-03:之前有 1 月 (10) + 2 月 (15) + 自身 (15) = 40

  • 第二张发票 2023-06:上次发票之后有 4 月 (10) + 5 月 (10) + 自身 (10) = 30

BBB 同理。

SQLazy 分步实现

|-----------|------------|-----------------------------------------------------------------------------------|
| Value | Anchor | Statement |
| t1 | invoice | sort id,dt desc |
| t2 | | compute invoiced cum as grp partition id |
| t3 | | summarize dt max as dt invoiced max as invoiced amount sum as amount group id grp |
| t4 | | derive id dt invoiced amount |

下面分别解释一下这些步骤。

第 1 步:按 ID 和日期排序,日期降序

sort id,dt desc

这一步是为了后续分组做准备。降序排列后,一个发票行与它前面的非开票行会先被处理,累计求和时这些行会被归入同一组。

第 2 步:对 invoiced 做累计求和,生成分组号

compute invoiced cum as grp partition id

在每个 ID 分区内,按当前顺序(降序)对 invoiced 进行累计求和(包括当前行)。结果如下:

第 3 步:按 ID 和 grp 分组汇总

summarize dt max as dt invoiced max as invoiced amount sum as amount group id grp

  • dt max:因为降序,每组内日期最大的是开票月份(发票行的日期)。

  • invoiced max:每组至少有一个 invoiced=1,所以最大值是 1。

  • amount sum:累加该组所有金额,即自该发票以来(包括自身)的总和。

第 4 步:选择需要的列

derive id dt invoiced amount

这一步只是清理输出,去掉辅助列 grp。

编译生成 SQL

完成上述步骤后,通过 SQLazy 的编译器可以生成等价的原生 SQL,无需手动编写。

这里生成 MySQL 语句:

复制代码
WITH t3 AS (
  SELECT
    id,
    grp,
    MAX(dt) AS dt,
    MAX(invoiced) AS invoiced,
    SUM(amount) AS amount
  FROM
    (
      SELECT
        id,
        dt,
        invoiced,
        amount,
        SUM(invoiced) OVER (
          PARTITION BY id
          ORDER BY
            CASE
              WHEN id IS NULL THEN 1
              ELSE 0
            END,
            id ASC,
            CASE
              WHEN dt IS NULL THEN 1
              ELSE 0
            END,
            dt DESC ROWS UNBOUNDED PRECEDING
        ) AS grp
      FROM
        invoice
    ) t2
  GROUP BY
    id,
    grp
)
SELECT
  id,
  dt,
  invoiced,
  amount
FROM
  t3
ORDER BY
  id,
  grp

你不需要读懂或调试这段 SQL,只需确认前面四步的逻辑正确,编译器就会输出可运行的代码。

列个表格总结一下:

|------|------------------------------------------------------|------------------|
| | 传统 SQL | SQLazy |
| 步骤数 | 多层 CTE + 窗口函数边界 | 4 步(排序→累计→汇总→选列) |
| 核心技巧 | 晦涩的 ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING | 直观的"降序 + 累计求和" |
| 可读性 | 低,需要理解偏移窗口 | 高,步骤即思考过程 |
| 调试方式 | 手动拆解 CTE,反复运行 | 单步执行,每一步中间结果可见 |
| 跨数据库 | 需手动调整方言差异 | 编译器自动生成对应语法 |

这个实例展示了 SQLazy 处理"按事件重置的累计"类问题的自然性:通过一个简单的排序技巧加上累计求和,就能把复杂的分组逻辑拆解为清晰的动作。

相关推荐
金伟API102411 小时前
不会写复杂SQL怎么办?
数据库·sql
geovindu12 小时前
sql:Search-Optimized SQL Queries using mysql 9.0
android·数据库·sql·mysql·数据库开发·数据库架构
这个DBA有点耶14 小时前
PostgreSQL HOT更新:频繁更新表的索引膨胀根因与fillfactor调优
数据库·sql·postgresql
一条小小yu17 小时前
Text-to-SQL Agent
数据库·sql
10mAh20 小时前
【SQL】NOT IN 遇到 NULL 为什么返回空集?三值逻辑与反关联实战
jvm·数据库·sql
todoitbo21 小时前
大数据平台如何选时序数据库:用业务 SQL 验证查询、留存和 Spark 链路
大数据·sql·时序数据库
事圆则缓1 天前
聊天与引用回复的 SQL 怎么设计
数据库·sql
l1t2 天前
DeepSeek总结的有符号、无符号、未对齐:从汇编到 SQL 的教训
汇编·数据库·sql
夜雪一千2 天前
MySQL 如何提高查询速度:从索引、SQL、表结构到实战调优
android·sql·mysql