窗口函数模拟FIFO扣减可行,但需满足批次有明确时间序且每批数量不可拆分;否则结果隐蔽出错。核心是逐批扣减、剩余归零、跳过耗尽批次,而非简单取前N条或硬编序号。

直接用窗口函数模拟 FIFO 扣减是可行的,但必须满足两个前提:批次有明确时间序(如 in_date)、每批数量可累加不可拆分。否则结果会错得隐蔽,且难以调试。
为什么不能只靠 ROW_NUMBER() 或 ORDER BY
FIFO 不是简单按时间取前 N 条,而是「逐批扣减、剩余归零、跳过已耗尽批次」的过程。只用 ORDER BY in_date 查前几条,会漏掉中间某批只剩 2 件、但下一批有 100 件的情况;ROW_NUMBER() 更危险——它给每行硬编序号,一旦某批已出完,这个序号就失去业务意义。
- 真实场景中,同一天可能有多批次入库,需按
in_date, batch_id复合排序,不能只依赖单字段 -
SUM(quantity) OVER (ORDER BY ...)累计和必须和当前行对齐,否则LEAST/GREATEST计算会跨批错位 - 若存在退货或红冲(即负向出入库),必须先做净变动聚合,再跑 FIFO,否则累计和逻辑崩溃
SUM() OVER () 累计和 + LAG() 做边界判断
核心思路是:对每个商品,按入库时间升序计算「截至本批次的累计入库量」,再与待出库量 Q 比较,找出被扣减覆盖的批次区间。关键不是“哪几行”,而是“每批实际出多少”。
WITH ranked AS (
SELECT
item_id,
batch_id,
in_date,
quantity,
SUM(quantity) OVER (
PARTITION BY item_id
ORDER BY in_date, batch_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cum_in
FROM stock_in
WHERE status = 'valid'
),
fetched AS (
SELECT *,
COALESCE(LAG(cum_in) OVER (PARTITION BY item_id ORDER BY in_date, batch_id), 0) AS prev_cum_in
FROM ranked
)
SELECT
item_id,
batch_id,
in_date,
CASE
WHEN cum_in = @q THEN 0 -- 完全没轮到
ELSE @q - prev_cum_in -- 本批只出一部分
END AS fifo_out_qty
FROM fetched
WHERE cum_in > @q OR prev_cum_in
-
@q是本次出库总量,必须作为参数传入,不能写死在 SQL 里 -
LAG(cum_in)提前拿到上一批的累计值,才能算出本批“从哪开始扣” - 结果中
fifo_out_qty可能为 0,说明该批未参与本次出库,但不能过滤掉——否则累计断层
遇到负库存或部分出库失败时怎么处理
上面逻辑默认库存充足。一旦 cum_in 最大值仍小于 @q,说明库存不足,但 SQL 不会自动报错。必须显式检查:
IF (SELECT MAX(cum_in) FROM ranked WHERE item_id = @item_id)
- 不能依赖应用层判断,因为并发出库时,两次查询之间库存可能已被其他事务扣减
- 更稳妥的做法是在事务中加
UPDLOCK, READPAST锁,防止重复扣减同一批次 - 如果系统支持,建议把 FIFO 扣减封装成带事务的存储过程,而非裸 SQL
真正难的不是写出这段 SQL,而是确保 stock_in 表里每条记录都代表一个不可再分的物理批次,且 in_date 和 batch_id 能唯一确定顺序。时间戳精度不够、批次号乱序、历史数据补录没校验——这些才是线上 FIFO 出错的根源,比函数用法重要得多。










