SQL Server 2019计算库存滚动数量/价值及移动平均单位成本
库存移动平均成本核算解决方案(SQL Server 2019)
实现思路
由于该计算属于逐行依赖前序结果的状态型计算,普通聚合窗口函数无法直接实现跨行状态传递,因此采用递归CTE+按tId有序迭代的方案实现,10万行数据量级下只要tId字段建有主键/索引,性能完全满足业务需求。
核心计算逻辑
- 滚动库存数量(Run_Qty):逐行累加tQty即可,不受单据类型影响
- 滚动库存价值(Run_Value):按单据类型区分计算规则
- PO_IN:前序滚动价值 + 当前入库成本
- SO_OUT:前序滚动价值 - 出库数量绝对值 * 前序平均成本
- FIN_ADJ:前序滚动价值 + 财务调整额
- 移动平均单位成本(avg_cost):当前滚动价值 / 当前滚动库存数量(库存为0时取0)
实现代码
WITH RecurInv AS ( -- 锚点:取最小tId作为第一行计算基准 SELECT TOP 1 tId, tType, tcode, tDate, tQty, tValue, CAST(tQty AS NUMERIC(19,6)) AS Run_Qty, CAST(tValue AS NUMERIC(19,6)) AS Run_Value, CASE WHEN tQty > 0 THEN CAST(tValue / tQty AS NUMERIC(19,6)) ELSE 0 END AS avg_cost FROM tst_Inv ORDER BY tId ASC UNION ALL -- 递归部分:逐行计算后续所有单据 SELECT curr.tId, curr.tType, curr.tcode, curr.tDate, curr.tQty, curr.tValue, -- 计算滚动库存数量 CAST(prev.Run_Qty + curr.tQty AS NUMERIC(19,6)) AS Run_Qty, -- 按类型计算滚动库存价值 CAST( CASE curr.tType WHEN 'PO_IN' THEN prev.Run_Value + curr.tValue WHEN 'SO_OUT' THEN prev.Run_Value - (ABS(curr.tQty) * prev.avg_cost) WHEN 'FIN_ADJ' THEN prev.Run_Value + curr.tValue END AS NUMERIC(19,6) ) AS Run_Value, -- 计算当前平均成本 CASE WHEN (prev.Run_Qty + curr.tQty) > 0 THEN CAST( (CASE curr.tType WHEN 'PO_IN' THEN prev.Run_Value + curr.tValue WHEN 'SO_OUT' THEN prev.Run_Value - (ABS(curr.tQty) * prev.avg_cost) WHEN 'FIN_ADJ' THEN prev.Run_Value + curr.tValue END) / (prev.Run_Qty + curr.tQty) AS NUMERIC(19,6)) ELSE 0 END AS avg_cost FROM RecurInv prev INNER JOIN tst_Inv curr ON curr.tId = prev.tId + 1 ) SELECT * FROM RecurInv ORDER BY tId ASC OPTION (MAXRECURSION 0); -- 取消递归次数限制,适配10万行以上数据
样例输出结果
| tId | tType | tcode | tDate | tQty | tValue | Run_Qty | Run_Value | avg_cost |
|---|---|---|---|---|---|---|---|---|
| 1 | PO_IN | 456 | 2021-09-01 | 200 | 3654.00 | 200.00 | 3654.000000 | 18.270000 |
| 2 | SO_OUT | 456 | 2021-09-03 | -155 | NULL | 45.00 | 822.150000 | 18.270000 |
| 3 | SO_OUT | 456 | 2021-09-04 | -15 | NULL | 30.00 | 548.100000 | 18.270000 |
| 4 | PO_IN | 456 | 2021-09-05 | 150 | 3257.00 | 180.00 | 3805.100000 | 21.139444 |
| 5 | SO_OUT | 456 | 2021-09-06 | -120 | NULL | 60.00 | 1268.366664 | 21.139444 |
| 6 | SO_OUT | 456 | 2021-09-07 | -10 | NULL | 50.00 | 1056.972224 | 21.139444 |
| 7 | FIN_ADJ | 456 | 2021-09-08 | 0 | -75.00 | 50.00 | 981.972224 | 19.639444 |
| 8 | SO_OUT | 456 | 2021-09-09 | -20 | NULL | 30.00 | 589.183336 | 19.639444 |
| 9 | PO_IN | 456 | 2021-09-02 | 5 | 0.00 | 35.00 | 589.183336 | 16.833810 |
| 10 | SO_OUT | 456 | 2021-09-10 | -35 | NULL | 0.00 | 0.000000 | 0.000000 |
性能优化建议
- 给tId字段创建主键聚集索引,递归关联时效率提升明显
- 多SKU核算的话,可以按tcode分区,先给每个SKU生成连续行号再做递归,避免跨SKU的计算干扰
内容的提问来源于stack exchange,提问作者GrahamH
相关产品推荐
相关产品推荐

