This is the classic month-over-month query:
SELECT
month,
revenue,
revenue - LAG(revenue) OVER (ORDER BY month) AS mom_change
FROM monthly_revenue;
LAG() looks back one row, not one month. If March has no rows, April gets compared with February, and nothing in the output warns you.
The fix: a month spine
Generate every month first, then join your data onto it. This example uses Postgres syntax:
WITH spine AS (
SELECT generate_series(
DATE '2026-01-01', DATE '2026-12-01', INTERVAL '1 month'
)::date AS month
),
filled AS (
SELECT s.month, COALESCE(r.revenue, 0) AS revenue
FROM spine s
LEFT JOIN monthly_revenue r ON r.month = s.month
)
SELECT
month,
revenue,
revenue - LAG(revenue) OVER (ORDER BY month) AS mom_change
FROM filled;
If you report by product or entity, cross join the spine with that dimension and add PARTITION BY to the window.
One judgment call: COALESCE(..., 0) says “no rows means zero.” If it could mean “data hasn’t loaded yet”, leave the NULL in place so the gap stays visible.