Step 1 Solve it yourself first
Tables you will use
What it really tests
Cohort-style thinking: anchor each user on their first event, look for a later event within a , and compute a rate — the core of product and growth analytics interviews.
The pattern: (first event, then a later event within N days)
"Retention", "came back", "returned within", "repeat purchase rate", "day-7 / day-30".
Picture it
flowchart LR F["first order per customer"] --> P["Priya: next order after 1 day"] F --> T["Tom: after 40 days"] F --> L["Lena: after 8 days"] F --> Z["Zoe: no second order"] P --> R["came back within 30 days:<br/>Priya, Lena<br/>2 of 4 = 50%"] L --> R
The approach, step by step
- Find each customer's first order date ( per customer).
- For each customer, check whether any later order falls within 30 days of that first date.
- Turn that into 1 or 0 per customer (MAX of a ).
- Rate = 100.0 × customers who came back ÷ customers who ordered.
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: first date, then a flag per customer
WITH firsts AS (
SELECT customer_id, MIN(order_date) AS first_date
FROM orders
GROUP BY customer_id
),
flags AS (
SELECT f.customer_id,
MAX(CASE WHEN o.order_date > f.first_date
AND julianday(o.order_date) - julianday(f.first_date) <= 30
THEN 1 ELSE 0 END) AS came_back
FROM firsts f
JOIN orders o ON o.customer_id = f.customer_id
GROUP BY f.customer_id
)
SELECT COUNT(*) AS customers,
SUM(came_back) AS came_back_within_30_days,
ROUND(100.0 * SUM(came_back) / COUNT(*), 1) AS retention_pct
FROM flags;1 row
| customers | came_back_within_30_days | retention_pct |
|---|---|---|
| 4 | 2 | 50 |
Another way: See it per customer with
WITH numbered AS (
SELECT customer_id, order_date,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date) AS rn,
LEAD(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) AS next_date
FROM orders
)
SELECT c.name, n.order_date AS first_order, n.next_date AS second_order,
julianday(n.next_date) - julianday(n.order_date) AS gap_days
FROM numbered n
JOIN customers c ON c.customer_id = n.customer_id
WHERE n.rn = 1
ORDER BY c.customer_id;4 rows
| name | first_order | second_order | gap_days |
|---|---|---|---|
| Priya | 2024-01-06 | 2024-01-07 | 1 |
| Tom | 2024-01-21 | 2024-03-01 | 40 |
| Lena | 2024-02-12 | 2024-02-20 | 8 |
| Zoe | 2024-03-16 | NULL | NULL |
The gap to the second order shows the answer at a glance: 1 and 8 days are within 30; 40 isn't; Zoe never came back.
Edge cases to say out loud
- A second order on the same day as the first: does it count as "coming back"? The solution says no (
>); ask. - Customers whose first order is less than 30 days before the end of the data haven't had a full 30 days yet — exclude them from the denominator for a fair rate.
- Should cancelled orders count as activity? Filter by status if not.
Common mistakes
- Counting any second order, regardless of how long after.
- Dividing whole numbers and getting 0.
- Measuring from sign-up date when the question says first order (or the other way round).
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.
-
Watch
Retention and cohort analysis in SQL (video)
YouTube search, free
-
Practice
DataLemur — retention 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 each customer is reduced to a single 1 or 0 before the rate is calculated.
Show answer
Because the question is about customers, not orders: "what share of customers came back?". Every customer must count exactly once.
If you worked on order instead, Priya, with 5 orders, would count five times and her habits would swamp everyone else's. So each customer is first reduced to one row with a single flag: 1 if they came back within 30 days, 0 if not (MAX over their orders turns "any qualifying order" into one 1). Then the rate is simple: the sum of the flags (customers who came back) ÷ the count of rows (customers).
-
Writefrom a blank page, then run it
Compute the share of customers who came back within 7 days.
Show answer
Change 30 to 7. Only Priya came back within a week (her second order was the next day). Lena's came 8 days later and Tom's 40 days later, and Zoe has only one order. 1 out of 4 is 25.0%.
sqlWITH firsts AS ( SELECT customer_id, MIN(order_date) AS first_date FROM orders GROUP BY customer_id ), flags AS ( SELECT f.customer_id, MAX(CASE WHEN o.order_date > f.first_date AND julianday(o.order_date) - julianday(f.first_date) <= 7 THEN 1 ELSE 0 END) AS came_back FROM firsts f JOIN orders o ON o.customer_id = f.customer_id GROUP BY f.customer_id ) SELECT COUNT(*) AS customers, SUM(came_back) AS came_back_within_7_days, ROUND(100.0 * SUM(came_back) / COUNT(*), 1) AS retention_pct FROM flags;1 row
customers came_back_within_7_days retention_pct 4 1 25 -
Predictsay it before you run it
What would retention_pct be with a 45-day window?
Show answer
75.0. Tom now counts as well: his second order (1 March) came 40 days after his first (21 January), inside 45 days but outside 30. With Priya and Lena, that is 3 of the 4 customers. Zoe still has only one order.
sqlWITH firsts AS ( SELECT customer_id, MIN(order_date) AS first_date FROM orders GROUP BY customer_id ), flags AS ( SELECT f.customer_id, MAX(CASE WHEN o.order_date > f.first_date AND julianday(o.order_date) - julianday(f.first_date) <= 45 THEN 1 ELSE 0 END) AS came_back FROM firsts f JOIN orders o ON o.customer_id = f.customer_id GROUP BY f.customer_id ) SELECT COUNT(*) AS customers, SUM(came_back) AS came_back_within_45_days, ROUND(100.0 * SUM(came_back) / COUNT(*), 1) AS retention_pct FROM flags;1 row
customers came_back_within_45_days retention_pct 4 3 75 -
Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.
Change the solution so only delivered orders count as activity. Who changes?
Show answer
Filter to delivered orders once, in a first , and use it in both later steps, so the first order and the return visit are both delivered ones.
Who changes:
• Lena: her first order (106) was cancelled, so her first delivered order is 107, and she has none after it. She no longer came back.
• Zoe: her only order is pending, so she has no delivered orders and drops out of the customers counted entirely.
• Priya (came back) and Tom (didn't) are unchanged.The rate becomes 1 of 3: 33.3%. The below shows the flags per customer, to make the change visible.
sqlWITH delivered AS ( SELECT customer_id, order_date FROM orders WHERE status = 'delivered' ), firsts AS ( SELECT customer_id, MIN(order_date) AS first_date FROM delivered GROUP BY customer_id ), flags AS ( SELECT f.customer_id, MAX(CASE WHEN d.order_date > f.first_date AND julianday(d.order_date) - julianday(f.first_date) <= 30 THEN 1 ELSE 0 END) AS came_back FROM firsts f JOIN delivered d ON d.customer_id = f.customer_id GROUP BY f.customer_id ) SELECT c.name, fl.came_back FROM flags fl JOIN customers c ON c.customer_id = fl.customer_id ORDER BY c.customer_id;3 rows
name came_back Priya 1 Tom 0 Lena 0
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 anchor each customer on their first order with MIN per customer, back to their orders, and flag whether any later order falls within 30 days, taking MAX of a CASE so each customer is one or zero. The retention rate is the sum of flags over the number of customers, times 100.0. I'd raise two definitional points: whether a same-day second order counts, and excluding customers who haven't had a full 30 days yet.
Follow-up questions
How would you compute retention by sign-up month (a )?
Add the first-order month to the firsts CTE, group the flags by that month, and compute the rate per cohort.
Why exclude recent customers from the denominator?
Someone who ordered yesterday hasn't had 30 days to come back; counting them as "not retained" makes the rate look worse than it is.
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.