直接用sum()累计会出错,因期初余额需显式注入:先用union all插入initial行(如2024-01-01, 5000),再用first_value取首行期初值,sum()算累计变动,期末=期初+累计,期初=lag(期末)或首行值;日期缺失须先用generate_series补全再合并。

为什么直接用 SUM() 累计会出错?
很多人一上来就写 SUM(balance) OVER (ORDER BY trans_date),结果发现余额对不上。问题在于:银行账户的期初/期末余额不是对某字段简单累加,而是基于「期初余额 + 当日所有交易净额」推导出来的。如果原始表里只有每笔交易的 amount(正为存、负为取),没有每日汇总行,SUM(amount) 确实能算累计变动,但无法直接对应到“某日开始前”和“某日结束后”这两个时点。更关键的是,窗口函数默认不处理“期初值注入”——你得显式把期初余额作为第一行的基准。
必须先补上期初余额这一行
真实场景中,期初余额不会出现在交易流水表里,它是一个独立的静态值(比如 2024-01-01 的期初是 5000)。所以第一步不是写窗口函数,而是用 UNION ALL 把它“插”进数据流最前面:
SELECT '2024-01-01'::DATE AS trans_date, 'INITIAL' AS trans_type, 5000.00 AS amount UNION ALL SELECT trans_date, trans_type, amount FROM transactions ORDER BY trans_date
注意三点:
• trans_date 类型要统一(建议显式转成 DATE)
• INITIAL 行的 amount 就是期初余额,不是 0
• 必须在 ORDER BY 前完成合并,否则窗口函数排序会乱
FIRST_VALUE() 和累计 SUM() 要分开用
期末余额 = 期初 + 截至当日所有交易之和;期初余额 = 期末余额的上一行值。但别用 LAG() 直接取上一行——因为第一行是 INITIAL,第二行的期初才等于第一行的期末。正确做法是:
- 用
FIRST_VALUE(amount) OVER (ORDER BY trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)提取首行的期初值(即 5000) - 用
SUM(amount) OVER (ORDER BY trans_date)算累计变动(从第一行开始累加) - 期末余额 =
FIRST_VALUE(...) + SUM(...) - 期初余额 =
COALESCE(LAG(ending_balance) OVER (ORDER BY trans_date), FIRST_VALUE(...))
重点:不能省略 COALESCE,否则第一行的期初会是 NULL;LAG 必须作用在已算出的 ending_balance 上,而不是原始 amount。
日期缺失会导致余额断层,得用生成序列补全
如果某天没交易,那日的期初/期末就没了。但银行报表要求按日连续。这时候不能靠 LEFT JOIN 原表——得先用 GENERATE_SERIES()(PostgreSQL)或递归 CTE(MySQL 8.0+/SQL Server)造出完整日期序列,再 LEFT JOIN 交易数据,把空日的 amount 设为 0。否则窗口函数会在缺失日跳过计算,导致后续所有余额偏移。
例如 PostgreSQL 中补全 2024-01-01 到 2024-01-10:
SELECT d.dt::DATE AS trans_date, COALESCE(t.amount, 0) AS amount
FROM GENERATE_SERIES('2024-01-01'::DATE, '2024-01-10'::DATE, '1 day') AS d(dt)
LEFT JOIN transactions t ON d.dt = t.trans_date
这步必须在插入 INITIAL 行之前做,否则生成的日期会比 INITIAL 行还早,破坏时序逻辑。
真正难的不是写对那几行窗口函数,而是理清“期初”“当日交易”“期末”三者的时间归属关系——它决定了你往哪插数据、在哪设默认值、对哪个字段做 LAG。漏掉任意一个时点定义,余额链就会断。











