SQL · medium
Asked in BI Developer interviews, in the SQL round.
Aggregate to month, then LAG the metric over ordered months: (this_month - LAG(metric) OVER (ORDER BY month)) / LAG(metric) OVER (ORDER BY month). Guard against divide-by-zero, and join to a month spine so a missing month reads as a gap rather than being silently skipped.
Aggregate to the month, then compare each month with the one before it using LAG.
WITH months AS (
SELECT generate_series(DATE '2025-01-01', DATE '2026-08-01', INTERVAL '1 month')::date AS month
),
monthly AS (
SELECT DATE_TRUNC('month', order_date)::date AS month, SUM(amount) AS revenue
FROM orders
GROUP BY 1
),
series AS (
SELECT m.month, COALESCE(r.revenue, 0) AS revenue
FROM months m
LEFT JOIN monthly r ON r.month = m.month
),
with_prev AS (
SELECT month, revenue, LAG(revenue) OVER (ORDER BY month) AS prev_revenue
FROM series
)
SELECT
month,
revenue,
prev_revenue,
CASE
WHEN prev_revenue IS NULL OR prev_revenue = 0 THEN NULL
ELSE (revenue - prev_revenue) / prev_revenue
END AS mom_growth
FROM with_prev
ORDER BY month;
Three details make it dashboard-safe. The month spine, so a month with no orders appears as 0 instead of being skipped, which would make LAG compare against the wrong month. The divide-by-zero guard, which returns NULL rather than an error or infinity (and note that x IN (0, NULL) would not catch the NULL; it has to be IS NULL). And the DATE_TRUNC, so the grouping key is a real date the chart can sort, not a formatted string.
LAG over a complete series, and the guard.