What is inventory turnover, and how do you measure it in Metabase?
Inventory turnover is how many times per year you sell through your average stock — cost of goods sold ÷ average inventory value, annualized. It's the metric that connects merchandising to cash: every point of turnover you gain is working capital released from the warehouse. Measure it in Metabase from order and inventory data synced into a database (Shopify, WooCommerce, or Square).
How is inventory turnover calculated?
COGS over a period ÷ average inventory value across that period, scaled to a year (multiply a quarterly ratio by 4). The choices that matter:
- COGS, not revenue — inventory is valued at cost, so a revenue numerator inflates the ratio by your markup.
- Average inventory, not point-in-time — average the period's daily (or at least weekly) snapshot values, or a restock the day before quarter-end distorts everything.
- Days of inventory (DSI) — 365 ÷ turnover converts the ratio into the operator's unit: how many days current stock lasts at the current sell-through rate.
- Segment it — a healthy blended turnover routinely hides dead stock in one category funding fast movers in another.
What data does inventory turnover need?
- Order line items with
quantityandunit_costfor COGS (unit costs live on products or purchase orders — join them in). - An
inventory_snapshotstable: one row per product per day withon_handandunit_cost, written by a scheduled job. Platform syncs usually carry only current stock, and history can't be backfilled. - Product category and SKU fields for segmentation and slow-mover lists.
- Order status, so cancelled and refunded orders don't count as sell-through.
SQL patterns
COGS per quarter over average snapshot value, scaled by 4.
-- Quarterly inventory turnover, annualized:
-- COGS from sold line items over average inventory value from daily snapshots.
WITH cogs AS (
SELECT
date_trunc('quarter', o.created_at) AS quarter,
SUM(li.quantity * li.unit_cost) AS cogs
FROM order_line_items li
JOIN orders o ON o.id = li.order_id
WHERE o.status NOT IN ('cancelled', 'refunded')
GROUP BY date_trunc('quarter', o.created_at)
),
inventory AS (
SELECT
date_trunc('quarter', snapshot_date) AS quarter,
AVG(daily_value) AS avg_inventory_value
FROM (
SELECT
snapshot_date,
SUM(on_hand * unit_cost) AS daily_value
FROM inventory_snapshots
GROUP BY snapshot_date
) daily
GROUP BY date_trunc('quarter', snapshot_date)
)
SELECT
c.quarter,
ROUND(c.cogs, 0) AS cogs,
ROUND(i.avg_inventory_value, 0) AS avg_inventory_value,
ROUND(4.0 * c.cogs / NULLIF(i.avg_inventory_value, 0), 2)
AS annualized_turnover
FROM cogs c
JOIN inventory i ON i.quarter = c.quarter
ORDER BY c.quarter;Rank categories by how long their stock lasts — the top of this list is your overstock.
-- Days of inventory (DSI) by category over the trailing 90 days:
-- how long the current stock would last at the recent sell-through rate.
WITH cogs AS (
SELECT
p.category,
SUM(li.quantity * li.unit_cost) AS cogs_90d
FROM order_line_items li
JOIN orders o ON o.id = li.order_id
JOIN products p ON p.id = li.product_id
WHERE o.created_at >= CURRENT_DATE - INTERVAL '90 days'
AND o.status NOT IN ('cancelled', 'refunded')
GROUP BY p.category
),
inventory AS (
SELECT
category,
AVG(daily_value) AS avg_inventory_value
FROM (
SELECT
p.category,
s.snapshot_date,
SUM(s.on_hand * s.unit_cost) AS daily_value
FROM inventory_snapshots s
JOIN products p ON p.id = s.product_id
WHERE s.snapshot_date >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY p.category, s.snapshot_date
) daily
GROUP BY category
)
SELECT
i.category,
ROUND(i.avg_inventory_value, 0) AS avg_inventory_value,
ROUND(90.0 * i.avg_inventory_value / NULLIF(c.cogs_90d, 0), 0)
AS days_of_inventory
FROM inventory i
JOIN cogs c ON c.category = i.category
ORDER BY days_of_inventory DESC;Pitfalls
Where this metric applies
- Shopify + Metabase — inventory levels, product costs, and order line items
- WooCommerce + Metabase — stock and order data in WordPress
- Square + Metabase — retail inventory counts and catalog costs
- BigCommerce + Metabase — product stock and order history
Related
Analytics
Metrics
FAQ
Should I use COGS or revenue in the numerator?
What are days of inventory (DSI), and which should I report?
What's a good inventory turnover ratio?
Why do I need inventory snapshots?
inventory_snapshots table with a date column. The metric only becomes computable from the day you start snapshotting, so start before you need it.