Step 1 Solve it yourself first
Tables you will use
What it really tests
Picking exactly one per group (the first event per user) with , and joining back for extra . Used for activation, onboarding and funnel questions.
The pattern: First (or latest) row per group
"First purchase", "most recent login", "latest status", "earliest", one per user.
Picture it
flowchart LR O["Priya's orders by date<br/>101 · 102 · 103 · 105 · 109"] --> R["ROW_NUMBER<br/>1 · 2 · 3 · 4 · 5"] R --> K["keep 1:<br/>order 101 on 2024-01-06"]
The approach, step by step
- Number each customer's orders by date:
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date, order_id). - Add order_id as a in case two orders share a date.
- Keep row number 1 in an outer .
- Join customers for the name and sign-up date, and work out the days between.
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: ROW_NUMBER = 1
WITH numbered AS (
SELECT customer_id, order_id, order_date, amount,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date, order_id) AS rn
FROM orders
)
SELECT c.name, n.order_id, n.order_date, n.amount,
julianday(n.order_date) - julianday(c.signup_date) AS days_to_first_order
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 | order_id | order_date | amount | days_to_first_order |
|---|---|---|---|---|
| Priya | 101 | 2024-01-06 | 120 | 4 |
| Tom | 104 | 2024-01-21 | 200 | 1 |
| Lena | 106 | 2024-02-12 | NULL | 13 |
| Zoe | 110 | 2024-03-16 | 33 | 6 |
Lena's first order is the cancelled one with no amount. Ask whether "first order" should mean first delivered order.
Another way: date, then match it
SELECT c.name, o.order_id, o.order_date, o.amount
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
WHERE o.order_date = (
SELECT MIN(o2.order_date)
FROM orders o2
WHERE o2.customer_id = o.customer_id
)
ORDER BY c.customer_id;4 rows
| name | order_id | order_date | amount |
|---|---|---|---|
| Priya | 101 | 2024-01-06 | 120 |
| Tom | 104 | 2024-01-21 | 200 |
| Lena | 106 | 2024-02-12 | NULL |
| Zoe | 110 | 2024-03-16 | 33 |
Returns two rows for a customer with two orders on their first day. ROW_NUMBER with a tiebreaker guarantees one.
Edge cases to say out loud
- Two orders on the same first day: the tiebreaker (order_id) decides; say which one you keep.
- Cancelled first orders: confirm whether they count.
- Customers with no orders: not in the result — from customers if they should appear.
Common mistakes
GROUP BY customer_idwithMIN(order_date)andamountin — the amount isn't tied to the first order.- Forgetting , so only one order in the whole gets number 1.
- No tiebreaker in ROW_NUMBER, so the answer can change between runs.
In other databases
- has a shortcut:
SELECT DISTINCT ON (customer_id) * FROM orders ORDER BY customer_id, order_date, order_id. - Days between: PostgreSQL
order_date - signup_datefor columns;DATEDIFF(order_date, signup_date).
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.
-
Practice
LeetCode — Top SQL 50 study plan
free account
-
Practice
DataLemur — first purchase 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 ROW_NUMBER needs a tiebreaker here, with an example.
Show answer
ROW_NUMBER must give exactly one row number 1 per customer. If two orders share the earliest date, ordering by date alone leaves the tie unbroken, and the may pick either one, and pick differently on another run or after an changes. The answer looks fine but isn't repeatable.
Example: if Priya had placed orders 101 and 111 both on 2024-01-06,
ORDER BY order_datecould return either as her first order. Addingorder_idas a second sort key (ORDER BY order_date, order_id) makes 101 win every time. -
Writefrom a blank page, then run it
For each customer, show their latest order and its status.
Show answer
Number each customer's orders newest first, with order_id as the tiebreaker, and keep number 1. Zoe's latest (and only) order is still pending.
sqlWITH numbered AS ( SELECT customer_id, order_id, order_date, status, ROW_NUMBER() OVER ( PARTITION BY customer_id ORDER BY order_date DESC, order_id DESC ) AS rn FROM orders ) SELECT c.name, n.order_id, n.order_date, n.status 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 order_id order_date status Priya 109 2024-03-10 delivered Tom 108 2024-03-01 delivered Lena 107 2024-02-20 delivered Zoe 110 2024-03-16 pending -
Predictsay it before you run it
What would the solution return for Lena if the only numbered delivered orders?
Show answer
Lena's first delivered order is 107 (20 February, £60), not the cancelled 106, so her days to first order become 21 instead of 13 (she signed up on 30 January).
And Zoe disappears: her only order is pending, so she has no delivered orders to number.
sqlWITH numbered AS ( SELECT customer_id, order_id, order_date, amount, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date, order_id) AS rn FROM orders WHERE status = 'delivered' ) SELECT c.name, n.order_id, n.order_date, n.amount, julianday(n.order_date) - julianday(c.signup_date) AS days_to_first_order FROM numbered n JOIN customers c ON c.customer_id = n.customer_id WHERE n.rn = 1 ORDER BY c.customer_id;3 rows
name order_id order_date amount days_to_first_order Priya 101 2024-01-06 120 4 Tom 104 2024-01-21 200 1 Lena 107 2024-02-20 60 21 -
Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.
Change the solution so customers with no orders appear with .
Show answer
Start from customers and LEFT JOIN the numbered orders. The
n.rn = 1test must go in the : in the , it would be NULL for customers with no orders and would drop them again. Omar and the second Tom now appear with NULLs.sqlWITH numbered AS ( SELECT customer_id, order_id, order_date, amount, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date, order_id) AS rn FROM orders ) SELECT c.name, n.order_id, n.order_date, n.amount, julianday(n.order_date) - julianday(c.signup_date) AS days_to_first_order FROM customers c LEFT JOIN numbered n ON n.customer_id = c.customer_id AND n.rn = 1 ORDER BY c.customer_id;6 rows
name order_id order_date amount days_to_first_order Priya 101 2024-01-06 120 4 Tom 104 2024-01-21 200 1 Lena 106 2024-02-12 NULL 13 Omar NULL NULL NULL NULL Zoe 110 2024-03-16 33 6 Tom NULL NULL NULL NULL
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.
For first-per-group I number rows with ROW_NUMBER partitioned by customer and ordered by date with the order id as a tiebreaker, keep row number one in an outer query, and customers for the sign-up date to compute days to first order. I'd confirm whether cancelled orders count, and mention that a MIN-date join can return duplicates on ties while ROW_NUMBER always returns exactly one.
Follow-up questions
How would you get each customer's latest order instead?
Order the ROW_NUMBER by order_date DESC, order_id DESC and keep row 1.
How would you find each customer's first delivered order?
Add WHERE status = 'delivered' inside the CTE, before numbering.
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.