A Hyper-Practical, Zero-Fluff Study Guide
What it is:Year-over-Year (YoY) and Month-over-Month (MoM) comparisons are SQL techniques to measure growth (or decline) by comparing metrics (revenue, users, churn, etc.) against the same period in the past. Think of it like a time machine for your data—you’re asking, "How did we do this month compared to last month?" or "How does Q1 2024 compare to Q1 2023?"
Why it matters in production:- Executives live by these numbers. If you can’t answer "Why did revenue drop 15% YoY?" in a board meeting, you’re toast.- Marketing teams use MoM to track campaign performance. Did that Super Bowl ad actually work? - Finance teams rely on YoY for forecasting. If you miscalculate YoY growth, budgets get slashed (or wasted).- E-commerce sites use MoM to detect anomalies. A sudden 30% MoM drop in sales? Probably a broken checkout button.
Real-world scenario:You’re a data analyst at an e-commerce company. The CEO asks: "Our revenue was $1.2M in March 2024. How does that compare to March 2023? And how did we grow from February 2024?" If you can’t answer this instantly with SQL, you’re not just slow—you’re invisible.
LAG()
DATE_TRUNC('month', order_date)
2024-03-15
2024-03-01
2023-03-10
2023-03-01
(current_value - previous_value) / previous_value * 100
19.8765%
20%
WITH
NULL
COALESCE(revenue, 0)
(100 - NULL) / NULL = NULL
orders
order_date
revenue
We’ll use a sample orders table with this schema:
CREATE TABLE orders ( order_id INT, customer_id INT, order_date DATE, revenue DECIMAL(10,2) );
-- Insert 3 years of sample data (2022-2024) INSERT INTO orders (order_id, customer_id, order_date, revenue) VALUES (1, 101, '2022-01-15', 100.00), (2, 102, '2022-02-20', 150.00), (3, 103, '2022-03-10', 200.00), (4, 104, '2023-01-05', 120.00), (5, 105, '2023-02-18', 180.00), (6, 106, '2023-03-25', 250.00), (7, 107, '2024-01-12', 130.00), (8, 108, '2024-02-28', 200.00), (9, 109, '2024-03-15', 300.00);
SELECT DATE_TRUNC('month', order_date) AS month, SUM(revenue) AS monthly_revenue FROM orders GROUP BY DATE_TRUNC('month', order_date) ORDER BY month;
Expected Output:
month | monthly_revenue ------------+----------------- 2022-01-01 | 100.00 2022-02-01 | 150.00 2022-03-01 | 200.00 2023-01-01 | 120.00 2023-02-01 | 180.00 2023-03-01 | 250.00 2024-01-01 | 130.00 2024-02-01 | 200.00 2024-03-01 | 300.00
WITH monthly_revenue AS ( SELECT DATE_TRUNC('month', order_date) AS month, SUM(revenue) AS revenue FROM orders GROUP BY DATE_TRUNC('month', order_date) ) SELECT month, revenue, LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue, LAG(revenue, 12) OVER (ORDER BY month) AS prev_year_revenue, ROUND((revenue - LAG(revenue, 1) OVER (ORDER BY month)) / NULLIF(LAG(revenue, 1) OVER (ORDER BY month), 0) * 100, 1) AS mom_growth_pct, ROUND((revenue - LAG(revenue, 12) OVER (ORDER BY month)) / NULLIF(LAG(revenue, 12) OVER (ORDER BY month), 0) * 100, 1) AS yoy_growth_pct FROM monthly_revenue ORDER BY month;
month | revenue | prev_month_revenue | prev_year_revenue | mom_growth_pct | yoy_growth_pct ------------+---------+--------------------+-------------------+----------------+--------------- 2022-01-01 | 100.00 | NULL | NULL | NULL | NULL 2022-02-01 | 150.00 | 100.00 | NULL | 50.0 | NULL 2022-03-01 | 200.00 | 150.00 | NULL | 33.3 | NULL 2023-01-01 | 120.00 | 200.00 | 100.00 | -40.0 | 20.0 2023-02-01 | 180.00 | 120.00 | 150.00 | 50.0 | 20.0 2023-03-01 | 250.00 | 180.00 | 200.00 | 38.9 | 25.0 2024-01-01 | 130.00 | 250.00 | 120.00 | -48.0 | 8.3 2024-02-01 | 200.00 | 130.00 | 180.00 | 53.8 | 11.1 2024-03-01 | 300.00 | 200.00 | 250.00 | 50.0 | 20.0
WITH monthly_revenue AS ( SELECT DATE_TRUNC('month', order_date) AS month, SUM(revenue) AS revenue FROM orders GROUP BY DATE_TRUNC('month', order_date) ), growth_calcs AS ( SELECT month, revenue, LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue, LAG(revenue, 12) OVER (ORDER BY month) AS prev_year_revenue, ROUND((revenue - LAG(revenue, 1) OVER (ORDER BY month)) / NULLIF(LAG(revenue, 1) OVER (ORDER BY month), 0) * 100, 1) AS mom_growth_pct, ROUND((revenue - LAG(revenue, 12) OVER (ORDER BY month)) / NULLIF(LAG(revenue, 12) OVER (ORDER BY month), 0) * 100, 1) AS yoy_growth_pct FROM monthly_revenue ) SELECT month, revenue, mom_growth_pct, yoy_growth_pct FROM growth_calcs WHERE prev_month_revenue IS NOT NULL AND prev_year_revenue IS NOT NULL ORDER BY month;
Expected Output (No NULLs):
month | revenue | mom_growth_pct | yoy_growth_pct ------------+---------+----------------+--------------- 2023-01-01 | 120.00 | -40.0 | 20.0 2023-02-01 | 180.00 | 50.0 | 20.0 2023-03-01 | 250.00 | 38.9 | 25.0 2024-01-01 | 130.00 | -48.0 | 8.3 2024-02-01 | 200.00 | 53.8 | 11.1 2024-03-01 | 300.00 | 50.0 | 20.0
monthly_revenue
growth_calcs
COALESCE
NULLIF
mom_growth_pct
Infinity
NULLIF(previous_value, 0)
prev_year_revenue IS NULL
LAG(12)
JOIN
✅ LAG() (correct answer)
"How do you calculate YoY growth for March 2024 vs. March 2023?"
LAG(revenue, 1)
✅ LAG(revenue, 12) (compares to March 2023)
LAG(revenue, 12)
"Why does this query return NULL for growth rates?" sql SELECT (revenue - LAG(revenue) OVER (ORDER BY month)) / LAG(revenue) OVER (ORDER BY month) * 100 FROM monthly_revenue;
sql SELECT (revenue - LAG(revenue) OVER (ORDER BY month)) / LAG(revenue) OVER (ORDER BY month) * 100 FROM monthly_revenue;
LAG(revenue) = 0
NULLIF(LAG(revenue), 0)
LAG(1)
DATE_TRUNC('month')
EXTRACT(MONTH FROM date)
DATE_TRUNC
EXTRACT
3
0
NULLIF(revenue, 0)
Challenge:Write a query that calculates YoY growth for Q1 (Jan-Mar) 2024 vs. Q1 2023, using the orders table from earlier.
Solution:
WITH quarterly_revenue AS ( SELECT DATE_TRUNC('quarter', order_date) AS quarter, SUM(revenue) AS revenue FROM orders GROUP BY DATE_TRUNC('quarter', order_date) ), growth_calcs AS ( SELECT quarter, revenue, LAG(revenue, 4) OVER (ORDER BY quarter) AS prev_year_revenue, ROUND((revenue - LAG(revenue, 4) OVER (ORDER BY quarter)) / NULLIF(LAG(revenue, 4) OVER (ORDER BY quarter), 0) * 100, 1) AS yoy_growth_pct FROM quarterly_revenue ) SELECT quarter, revenue, yoy_growth_pct FROM growth_calcs WHERE prev_year_revenue IS NOT NULL AND quarter = '2024-01-01'; -- Q1 2024
Why it works:- DATE_TRUNC('quarter', order_date) groups by quarter.- LAG(revenue, 4) compares to the same quarter last year (4 quarters = 1 year).- NULLIF prevents division by zero.
DATE_TRUNC('quarter', order_date)
LAG(revenue, 4)
LAG(revenue, 1) OVER (ORDER BY month)
LAG(revenue, 12) OVER (ORDER BY month)
(revenue - LAG(revenue, 1)) / NULLIF(LAG(revenue, 1), 0) * 100
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.