Safety Stock Calculation and Warehouse Capacity Analysis
Safety Stock Calculation and Warehouse Capacity Analysis
Safety stock is the buffer inventory held to protect against demand uncertainty and supplier delays. This lesson shows how to calculate it in SQL and combines it with warehouse capacity analysis.
1. Safety Stock Formula
Safety Stock = Z × σ_demand × √(lead_time)
Where:
- Z = service level factor (1.65 for 95% service level)
- σ_demand = standard deviation of demand
- lead_time = supplier lead time in days
We approximate demand variability using purchase order quantity variance:
WITH demand_stats AS (
SELECT
po.warehouse_id,
po.sku_id,
AVG(po.quantity) AS avg_demand,
STDDEV(po.quantity) AS stddev_demand,
sup.lead_time_days
FROM lg_purchase_orders po
JOIN lg_skus s ON s.id = po.sku_id
JOIN lg_suppliers sup ON sup.id = po.supplier_id
GROUP BY po.warehouse_id, po.sku_id, sup.lead_time_days
)
SELECT
w.code,
s.sku,
s.name,
i.quantity AS current_stock,
ROUND((ds.avg_demand)::NUMERIC, 0) AS avg_order_qty,
ROUND((ds.stddev_demand)::NUMERIC, 1) AS demand_stddev,
ds.lead_time_days,
ROUND((1.65 * COALESCE(ds.stddev_demand, 0) * SQRT(ds.lead_time_days))::NUMERIC, 0) AS safety_stock_95pct,
ROUND((1.28 * COALESCE(ds.stddev_demand, 0) * SQRT(ds.lead_time_days))::NUMERIC, 0) AS safety_stock_90pct
FROM demand_stats ds
JOIN lg_inventory i ON i.warehouse_id = ds.warehouse_id AND i.sku_id = ds.sku_id
JOIN lg_warehouses w ON w.id = i.warehouse_id
JOIN lg_skus s ON s.id = i.sku_id
ORDER BY safety_stock_95pct DESC;
2. Inventory Below Safety Stock
Flag SKUs where current stock is below the calculated safety stock:
WITH safety AS (
SELECT
po.warehouse_id,
po.sku_id,
ROUND((1.65 * COALESCE(STDDEV(po.quantity), 0)
* SQRT(MAX(sup.lead_time_days)))::NUMERIC, 0) AS safety_stock
FROM lg_purchase_orders po
JOIN lg_suppliers sup ON sup.id = po.supplier_id
GROUP BY po.warehouse_id, po.sku_id
)
SELECT
w.code,
s.sku,
s.name,
i.quantity AS current_stock,
sf.safety_stock,
sf.safety_stock - i.quantity AS deficit
FROM safety sf
JOIN lg_inventory i ON i.warehouse_id = sf.warehouse_id AND i.sku_id = sf.sku_id
JOIN lg_warehouses w ON w.id = i.warehouse_id
JOIN lg_skus s ON s.id = i.sku_id
WHERE i.quantity < sf.safety_stock
ORDER BY deficit DESC;
3. Warehouse Capacity Utilisation
Estimate space used based on SKU weight as a proxy for volume:
SELECT
w.code,
w.city,
w.capacity_m3,
SUM(i.quantity * s.weight_kg) AS total_weight_kg,
ROUND((100.0 * SUM(i.quantity * s.weight_kg) / NULLIF(w.capacity_m3 * 500, 0))::NUMERIC, 1) AS estimated_util_pct,
COUNT(DISTINCT i.sku_id) AS distinct_skus,
SUM(i.quantity * s.unit_cost) AS inventory_value_usd
FROM lg_inventory i
JOIN lg_warehouses w ON w.id = i.warehouse_id
JOIN lg_skus s ON s.id = i.sku_id
GROUP BY w.id, w.code, w.city, w.capacity_m3
ORDER BY estimated_util_pct DESC;
What This Returns
code | city | capacity_m3 | total_weight_kg | estimated_util_pct | inventory_value_usd
------+-------------+-------------+-----------------+--------------------+--------------------
US-NY | New York | 50000 | 12450.30 | 49.8 | 482300.00
...
4. Overstocked SKUs
Identify SKUs where stock > 3× the reorder quantity (over-purchasing risk):
SELECT
w.code,
s.sku,
s.name,
i.quantity,
200,
ROUND(i.quantity::NUMERIC / NULLIF(200, 0), 1) AS stock_to_reorder_ratio,
ROUND((s.unit_cost * (i.quantity - 200 * 2))::NUMERIC, 2) AS excess_inventory_value_usd
FROM lg_inventory i
JOIN lg_warehouses w ON w.id = i.warehouse_id
JOIN lg_skus s ON s.id = i.sku_id
WHERE i.quantity > 200 * 3
ORDER BY excess_inventory_value_usd DESC;
Key Takeaway
STDDEV(quantity) in PostgreSQL calculates sample standard deviation, which is appropriate for demand variability calculations. The √lead_time term in the safety stock formula accounts for the fact that variability compounds over longer supply chains. Always validate that STDDEV has enough data points (at least 10 orders) before using it — for sparse data, use the supplier's stated reliability_score to estimate variability instead.