FIFO库存计算的核心逻辑是按入库时间顺序逐批消耗,而非简单聚合;需用窗口函数计算滚动净量,通过累计和与LAG值判断每批实际消耗量与剩余量,避免JOIN导致的笛卡尔积。

什么是FIFO库存计算的核心逻辑
FIFO不是SQL内置函数,而是业务规则:同一物料的入库记录按created_at(或batch_no)升序排列,出库时优先消耗最早入库的批次。关键在于「按时间顺序逐批匹配」,不能简单用SUM()或GROUP BY聚合了事。
用窗口函数模拟批次消耗过程
主流做法是先对入库和出库做统一时间排序,再用累计和判断每批剩余量。常见错误是直接JOIN入库表和出库表——这会产生笛卡尔积,导致数量爆炸。
- 必须把入库和出库拆成两套记录,用
UNION ALL合并,并标记type IN ('in', 'out') - 按
material_id和created_at排序后,用SUM(amount) OVER (PARTITION BY material_id ORDER BY created_at, type ROWS UNBOUNDED PRECEDING)算滚动净量 - 入库记录的
amount为正,出库为负;滚动和首次≤0的位置,说明该入库批次已被完全消耗
示例片段(PostgreSQL):
SELECT material_id,
batch_no,
amount AS in_amount,
COALESCE(
LEAD(cumsum) OVER (PARTITION BY material_id ORDER BY created_at, type) - cumsum,
0
) AS remaining
FROM (
SELECT material_id, batch_no, created_at, 'in'::text AS type, amount,
SUM(amount) OVER (PARTITION BY material_id ORDER BY created_at, type ROWS UNBOUNDED PRECEDING) AS cumsum
FROM inventory_in
UNION ALL
SELECT material_id, NULL, created_at, 'out', -amount
FROM inventory_out
) t;处理部分消耗和边界情况
真实场景中,一笔出库常会跨多个入库批次。比如:入库[100, 200, 150],出库280 → 第一批剩0、第二批剩20、第三批剩150。这时不能只看累计和符号,得算「本批可消耗量」。
- 用
LAG(cumsum, 1, 0) OVER (...)拿到上一行累计值,当前行cumsum为负时,本批实际消耗 = 上一行cumsum(正值) -
remaining = GREATEST(0, in_amount - GREATEST(0, LAG(cumsum, 1, 0) OVER (...) - COALESCE(LAG(in_amount) OVER (...), 0)))太绕?不如分步:先算每笔出库分配到各入库批次的量,再反推剩余 - 注意NULL处理:
inventory_out通常无batch_no,LEFT JOIN时别漏掉WHERE in_batch IS NOT NULL
性能与数据一致性要点
单次查询跑FIFO逻辑在万级记录内可行,但频繁调用会拖慢报表。容易被忽略的是:事务未提交的in/out记录是否参与计算?
- 务必加
WHERE status = 'confirmed'(或类似字段),避免草稿单干扰 - 如需实时性,考虑用物化视图预计算每日FIFO快照,而非每次查全量
- MySQL 8.0+ 支持窗口函数,但
ROWS UNBOUNDED PRECEDING在大表上仍可能触发临时表;PostgreSQL建议给(material_id, created_at)建联合索引
最麻烦的其实是「逆向操作」:红冲单据怎么回滚FIFO状态。这没法靠纯SQL安全完成,得依赖应用层维护批次消耗映射表。

















