直接用 GROUP BY 算不出库存余额累计值,因为它会压缩行、丢失中间状态;必须用 SUM() OVER(PARTITION BY item_id ORDER BY transaction_time, id) 逐行累加净变动量,并合并期初记录。

为什么直接用 GROUP BY 算不出库存余额累计值
因为 GROUP BY 会压缩行,而库存余额是随时间逐笔变化的累积过程,需要保留每一笔出入库记录,并在每行上算出“截至当前单据的总余额”。窗口函数正是干这事的——它不聚合行,只在逻辑分组内按序计算。
常见错误是写成:SELECT SUM(qty) FROM stock_log GROUP BY item_id,这只能得出最终总量,丢失中间状态;或者误用 SUM(qty) OVER () 忘记加 ORDER BY,导致累计顺序错乱(窗口默认无序,SUM 会算全表总和,不是逐行累加)。
- 必须指定
ORDER BY transaction_time(或单据号、流水ID等能反映业务时序的字段) - 若同一时刻有多笔,需补充二级排序,例如:
ORDER BY transaction_time, id - 库存初始值不在日志表中?得用
UNION ALL补一条期初记录,再一起开窗
如何用 SUM() OVER() 正确计算逐笔余额
核心是构造一个“净变动量”字段:入库为正,出库为负。假设表 stock_log 有字段 item_id、type('in'/'out')、qty、transaction_time:
SELECT
item_id,
type,
qty,
transaction_time,
SUM(CASE WHEN type = 'in' THEN qty ELSE -qty END)
OVER (PARTITION BY item_id ORDER BY transaction_time, id) AS balance
FROM stock_log;
注意点:
-
PARTITION BY item_id保证不同商品互不影响 - 没写
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW?没关系,这是SUM() OVER()的默认帧(frame),但显式写出来更清晰,尤其后续要改滑动窗口时 - 如果
transaction_time有重复且没唯一排序依据,CURRENT ROW可能包含多行,导致某次更新“跳变”,务必加id或log_id防歧义
怎么处理期初库存并合并到累计结果里
多数库存系统有期初余额(opening stock),但它不在日常流水表中。硬编码进 SQL 不可持续,正确做法是把期初数据作为一行“虚拟单据”注入:
WITH init AS (
SELECT 'init' AS type, opening_qty AS qty, '2024-01-01 00:00:00'::TIMESTAMP AS transaction_time, item_id
FROM stock_opening
),
full_log AS (
SELECT item_id, type, qty, transaction_time FROM init
UNION ALL
SELECT item_id, type, qty, transaction_time FROM stock_log
)
SELECT
item_id,
type,
qty,
transaction_time,
SUM(CASE WHEN type = 'in' THEN qty ELSE -qty END)
OVER (PARTITION BY item_id ORDER BY transaction_time, type DESC) AS balance
FROM full_log;
关键细节:
- 期初时间设为早于所有实际单据(如
'2024-01-01'),避免排序错位 -
type DESC是为了确保当时间相同时,'init' 排在前面(ASCII 中 'i' > 'o'),让期初值成为累计起点 - 如果
stock_opening没有item_id字段,得先 LEFT JOIN 补全,否则 PARTITION BY 会漏项
性能和边界情况要注意什么
百万级流水表上跑 SUM() OVER() 很快,但慢往往出在排序阶段。真正卡住的是没索引或类型不匹配:
- 必须在
(item_id, transaction_time, id)上建联合索引,否则每次开窗都要临时排序 -
transaction_time是TEXT类型?转成TIMESTAMP再排序,否则字典序比较会导致 2024-10-01 排在 2024-2-01 前面 - 某条记录
qty为 NULL?CASE表达式结果也是 NULL,整条累计链就中断了——加COALESCE(qty, 0)更安全 - 需要查“某天结束时的余额”?别用 WHERE 过滤后开窗,那会丢掉之前的数据。应该开窗完再用子查询或 LATERAL 找最后一条
最易被忽略的一点:业务上“调拨单”可能同时影响两个仓库,但单条记录只体现一个方向。这种场景不能只靠 item_id 分区,得结合 warehouse_id,否则跨仓调拨会让余额对不上。










