安全库存警戒线不能用 GROUP BY 算,因其会丢失时序细节,无法兼顾历史波动与当前在途/在库状态;必须用窗口函数按 item_id 分区、日期排序,结合补货周期构建动态需求窗口计算标准差与移动平均。

安全库存警戒线为什么不能用 GROUP BY 算?
因为安全库存需要同时看到「历史波动」和「当前在途/在库」,而 GROUP BY 会压缩行、丢失时序细节。窗口函数才能在保留每条采购/入库/出库记录的前提下,动态计算滚动标准差、移动平均、累计在途量等关键中间值。
典型错误是写成:SELECT item_id, AVG(demand) AS avg_demand, STDDEV(demand) FROM sales GROUP BY item_id —— 这忽略了需求的时间聚集性(比如促销周 vs 淡季),也切不断与补货周期的对齐关系。
- 必须用
PARTITION BY item_id ORDER BY date构建时间有序分组 - 补货周期(lead_time)要转为
ROWS BETWEEN CURRENT ROW AND n FOLLOWING的偏移窗口,而非固定天数 - 标准差必须基于「未来 lead_time 天内」的需求样本,不是全量历史
用 WINDOW 定义带补货周期对齐的需求数列
安全库存公式通常为:Z * SQRT(lead_time * σ²_daily + μ_daily² * σ²_leadtime),但实际中常简化为 Z * σ_demand_in_lead_time * SQRT(lead_time)。关键是先算出每个日期对应的「未来 lead_time 天需求总和」及其标准差。
PostgreSQL / Snowflake / BigQuery 都支持命名窗口,推荐这样写:
WINDOW w AS ( PARTITION BY item_id ORDER BY sale_date ROWS BETWEEN CURRENT ROW AND :lead_time FOLLOWING )
注意::lead_time 必须是整数,且需提前从物料主数据表 JOIN 进来(不能写死);如果不同 SKU 补货周期不同,ROWS 无法动态取字段值,得改用 RANGE BETWEEN INTERVAL '7 days' FOLLOWING(仅部分引擎支持)。
- SQL Server 不支持
INTERVAL,得用DATEADD(day, lead_time, sale_date)配合子查询模拟 - MySQL 8.0+ 支持
RANGE,但要求排序字段是数值型,日期需转为UNIX_TIMESTAMP(sale_date) - 若 lead_time 是浮动的(如海运/空运混用),建议先用 CTE 预算每个订单行的「预期到货日」,再按该日期做窗口
避免 STDDEV 窗口函数返回 NULL 的三个条件
STDDEV() 窗口函数在样本数 NULL,这会导致整个安全库存值崩掉。常见于新上架 SKU 或断货期后的首单。
- 强制至少 2 个样本:加
CASE WHEN COUNT(*) OVER w < 2 THEN 0.1 ELSE STDDEV(daily_demand) OVER w END - 用
COALESCE(STDDEV(daily_demand) OVER w, 0.01)不够稳妥——当窗口内只有 1 行时,STDDEV不触发计算,直接返回 NULL,COALESCE也救不回来 - 更可靠的是先聚合再窗口:用
ARRAY_AGG(daily_demand) OVER w(BigQuery/Snowflake)或STRING_AGG拼接后在 UDF 中算标准差(适合复杂逻辑)
另外,原始销售数据含 0 值(休市、缺货)必须清洗——直接参与 STDDEV 会严重低估波动。建议先过滤 WHERE daily_demand > 0 或用中位数替代法插补。
最终警戒线字段要区分「理论值」和「业务值」
纯计算出的 safety_stock = 1.65 * stddev_demand_in_lt * SQRT(lead_time) 只是起点。真实系统里必须叠加业务规则:
- 最低保底:强制
GREATEST(safety_stock, 5)(避免小数件或 0) - 单位对齐:SKU 若只能整箱采购,需
CEIL(safety_stock / case_pack_qty) * case_pack_qty - 动态冻结:当
on_hand_qty + in_transit_qty < safety_stock时,触发采购单;但若in_transit_qty来自已取消订单,该字段不可信,得关联订单状态表过滤
最容易被忽略的是「lead_time 的粒度」:如果销售表是日级,但采购 LT 是工作日(不含周末),而你用 ROWS BETWEEN CURRENT ROW AND 7 FOLLOWING,就多算了 2 天需求。务必统一时间基准——要么销售表补全周末 0 值,要么把 LT 换算成自然日再对齐。

















