Step 1 Solve it yourself first
Tables you will use
What it really tests
Aggregating to the right first, then , then safe percentage maths — and division by zero included.
The pattern: Period-over-period change
"Growth", "change from the previous", "month over month", "compared with last week".
Picture it
flowchart LR O["orders"] --> M["monthly revenue<br/>Jan 445 · Feb 210 · Mar 138"] M --> L["LAG brings the month before<br/>Feb sees 445 · Mar sees 210"] L --> G["growth %<br/>Feb −52.8 · Mar −34.3"]
The approach, step by step
- orders to one row per month first (a ).
- Use
LAG(revenue) OVER (ORDER BY month)to bring last month's revenue onto each row. - Growth % = 100.0 × (this − last) ÷ last. Use 100.0 to avoid integer division and
NULLIF(last, 0)to avoid dividing by zero. - The first month has no previous month, so its growth is .
Your turn: write it before you look
Your queries in this box
Show the solution
Stuck for more than ten minutes? Open it, read it, close it, and write it again from memory.
Solution: monthly CTE, then LAG
WITH monthly AS (
SELECT strftime('%Y-%m', order_date) AS month,
SUM(amount) AS revenue
FROM orders
GROUP BY strftime('%Y-%m', order_date)
)
SELECT month, revenue,
ROUND(100.0 * (revenue - LAG(revenue) OVER (ORDER BY month))
/ LAG(revenue) OVER (ORDER BY month), 1) AS growth_pct
FROM monthly
ORDER BY month;3 rows
| month | revenue | growth_pct |
|---|---|---|
| 2024-01 | 445 | NULL |
| 2024-02 | 210 | -52.8 |
| 2024-03 | 138 | -34.3 |
Another way: A named , and a guard against dividing by zero
WITH monthly AS (
SELECT strftime('%Y-%m', order_date) AS month,
SUM(amount) AS revenue
FROM orders
GROUP BY strftime('%Y-%m', order_date)
)
SELECT month, revenue,
ROUND(100.0 * (revenue - LAG(revenue) OVER w)
/ NULLIF(LAG(revenue) OVER w, 0), 1) AS growth_pct
FROM monthly
WINDOW w AS (ORDER BY month)
ORDER BY month;3 rows
| month | revenue | growth_pct |
|---|---|---|
| 2024-01 | 445 | NULL |
| 2024-02 | 210 | -52.8 |
| 2024-03 | 138 | -34.3 |
WINDOW w (…) names the window once. turns a previous revenue of 0 into NULL, so the growth is NULL instead of an error.
Edge cases to say out loud
- The first month: no previous month, so NULL.
- A previous month with 0 revenue: divide by
NULLIF(…, 0). - A month with no orders at all has no row, so LAG would compare with two months ago — build a month list and if that matters.
Common mistakes
- Using LAG on the order rows instead of the monthly totals.
(revenue - prev) / prev * 100with whole numbers: integer division gives 0 or -1.- Grouping by order_date instead of the month.
In other databases
- :
DATE_TRUNC('month', order_date)for the month. :DATE_FORMAT(order_date, '%Y-%m'). :FORMAT(order_date, 'yyyy-MM')orDATEFROMPARTS(YEAR(d), MONTH(d), 1).
Step 2 Go further (free, optional)
Optional. The notes above are written to be enough on their own. These free resources give a second explanation and more practice.
- Read
-
Practice
DataLemur — month-over-month growth questions
real interview questions, many free
Step 3 Practise
Try each one yourself first: out loud, or written and run in the practice box below, which saves every query you run. Then open the answer and compare.
-
ExplainSay what it does and when to use it, in plain words, with no notes.
Explain why you aggregate to months before using LAG.
Show answer
LAG returns the previous row, so the rows must be the things you want to compare: months. On the raw orders , the previous row is the previous order, so "growth" would compare one order with another order.
So first collapse to one row per month ( month, (amount)) in a CTE, and only then use LAG over those monthly rows. The previous row is now the previous month.
(The order of the steps matters for a second reason too: window functions run after GROUP BY, so even in one , LAG over a SUM is computed on the grouped rows.)
-
Writefrom a blank page, then run it
Compute month-over-month growth in the number of orders, not revenue.
Show answer
Swap SUM(amount) for (*). January has 4 orders, February 3 (−25%) and March 3 (0%).
One question to ask: do cancelled orders count? Order 106 is in February's 3. Excluding it would make February 2 and March +50%.
sqlWITH monthly AS ( SELECT strftime('%Y-%m', order_date) AS month, COUNT(*) AS orders FROM orders GROUP BY strftime('%Y-%m', order_date) ) SELECT month, orders, ROUND(100.0 * (orders - LAG(orders) OVER (ORDER BY month)) / LAG(orders) OVER (ORDER BY month), 1) AS growth_pct FROM monthly ORDER BY month;3 rows
month orders growth_pct 2024-01 4 NULL 2024-02 3 -25 2024-03 3 0 -
Predictsay it before you run it
What would growth_pct be for March if March revenue were 315?
Show answer
+50.0. February's revenue is 210, so (315 − 210) ÷ 210 = 105 ÷ 210 = 0.5, which is 50%.
The query below makes March 315 by adding 177 to order 110 (138 + 177 = 315).
sqlUPDATE orders SET amount = amount + 177 WHERE order_id = 110; WITH monthly AS ( SELECT strftime('%Y-%m', order_date) AS month, SUM(amount) AS revenue FROM orders GROUP BY strftime('%Y-%m', order_date) ) SELECT month, revenue, ROUND(100.0 * (revenue - LAG(revenue) OVER (ORDER BY month)) / LAG(revenue) OVER (ORDER BY month), 1) AS growth_pct FROM monthly ORDER BY month;3 rows
month revenue growth_pct 2024-01 445 NULL 2024-02 210 -52.8 2024-03 315 50 -
Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.
Show what goes wrong if you remove the 100.0 and use whole-number maths, then fix it.
Show answer
Revenue is a whole number, so with whole-number maths the division throws away the fraction. (210 − 445) ÷ 445 is −0.53, which becomes 0, and 0 × 100 = 0: February shows 0% growth instead of −52.8%. March does the same. Nothing errors; the numbers are just wrong.
The fix is to make one side a decimal before dividing: multiply by 100.0 first, as the solution does. Multiplying by 100 (whole) first would be closer but still cut the decimals off: −52 instead of −52.8.
sqlWITH monthly AS ( SELECT strftime('%Y-%m', order_date) AS month, SUM(amount) AS revenue FROM orders GROUP BY strftime('%Y-%m', order_date) ) SELECT month, revenue, (revenue - LAG(revenue) OVER (ORDER BY month)) / LAG(revenue) OVER (ORDER BY month) * 100 AS whole_number_maths, ROUND(100.0 * (revenue - LAG(revenue) OVER (ORDER BY month)) / LAG(revenue) OVER (ORDER BY month), 1) AS growth_pct FROM monthly ORDER BY month;3 rows
month revenue whole_number_maths growth_pct 2024-01 445 NULL NULL 2024-02 210 0 -52.8 2024-03 138 0 -34.3
Practice box: write and run your answers here
Your queries in this box
Step 4 Interview-ready, then prove it
Your interview answer
About 20 to 40 seconds, said out loud.
I aggregate to one row per month in a CTE, then use LAG over month order to bring the previous month's revenue alongside, and compute 100.0 times the difference divided by the previous value, rounded. I guard the division with NULLIF for zero months, note that the first month is NULL, and would fill in missing months with a calendar if gaps are possible.
Follow-up questions
How would you do year-over-year growth on monthly data?
LAG(revenue, 12) OVER (ORDER BY month) — twelve rows back — assuming every month is present.
How would you compute it per product category?
Group by category and month, then LAG(revenue) OVER (PARTITION BY category ORDER BY month).
Tick each check when you can do it without notes
A topic is finished when you pass all four checks, not when you have read it. Be strict: if you had to look at the notes, it is not ticked yet.
- Explain Say what it does and when to use it, in plain words, with no notes.
- Write Write the query from a blank page, then run it to check.
- Predict Read a query and say which rows it returns before you run it.
- Edge cases Say what happens with NULLs, ties, duplicates or no rows at all, and fix it.
0 of 4 checks ticked.
Saved in this browser. Back it up or move it to another device.