Second Purchase
A medium SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.
- Domain
- SQL
- Difficulty
- medium
- Seniority
- L5
Problem
Someone who bought only once has no second purchase to study, so they should not appear at all. An onboarding analyst wants every other user's second purchase from the transactions table, going by transaction date, with that purchase's amount and date.
The first buy is curiosity. The second is commitment.
Tables
transactions
Columns: transaction_id INTEGER, user_id INTEGER, product_id INTEGER, quantity INTEGER, total_amount REAL, transaction_date TEXT
| transaction_id | user_id | product_id | quantity | total_amount | transaction_date |
|---|---|---|---|---|---|
| 1067 | 197 | 1001 | 2 | 23.46 | 2026-02-02 |
| 1134 | 294 | 1050 | 3 | 36.93 | 2026-03-03 |
| 1201 | 391 | 1099 | 4 | 50.4 | 2026-04-04 |
| 1268 | 488 | 1148 | 5 | 63.87 | 2026-05-05 |
| 1335 | 585 | 1197 | 0 | 0 | 2026-06-06 |
| 1402 | 682 | 1246 | 2 | 90.81 | 2026-07-07 |
| 1469 | 779 | 1295 | 3 | 104.28 | 2026-08-08 |
| 1536 | 876 | 1344 | 4 | 117.75 | 2026-09-09 |
| 1603 | 973 | 1393 | 5 | -45 | 2026-01-10 |
| 1670 | 1070 | 1442 | 1 | 144.69 | 2026-02-11 |
Expected output
| user_id | total_amount | transaction_date |
|---|---|---|
| 100 | 616.14 | 2026-01-18 |
| 197 | 831.66 | 2022-01-06 |
| 294 | 845.13 | 2023-02-07 |
| 391 | 858.6 | 2024-03-08 |
| 488 | 63.87 | 2025-04-05 |
Worked solution and explanation
Slot two, row intact
This is per-user positional selection dressed up as onboarding research. Out of every user's purchase history, keep the purchase sitting in slot two by date, and keep it whole: its total_amount and its transaction_date have to come from the same row. Anyone can find a user's first purchase with MIN(transaction_date). What separates candidates is numbering rows inside each user's history and then keeping exactly one of them. Collapse to one row per user and the second purchase is gone (user 197 comes back with its first purchase date, not its second). Number across the whole table instead and you get one purchase for the entire customer base instead of fifteen.
Getting there
Step 1: Number purchases inside each user
PARTITION BY user_id restarts the count for every user, and ORDER BY transaction_date decides who comes first. ROW_NUMBER() hands out 1, 2, 3 with no gaps and no repeats, so every user has at most one row numbered 2. The numbered CTE carries total_amount and transaction_date alongside rn, so the row you keep is the real purchase and not a reconstruction.
Step 2: Filter on the number one level up
Window results do not exist yet when WHERE runs, so WHERE ROW_NUMBER() OVER (...) = 2 is an error. Compute rn in numbered and filter rn = 2 in the outer query: number first, then select.
Step 3: Let single-purchase users fall away
A user with one purchase only ever gets rn = 1, so rn = 2 drops them with no extra clause. You need no HAVING COUNT(*) > 1 and no outer join that would produce a NULL second purchase: the missing slot-two row is the exclusion. On this data every one of the 15 buyers has a dozen or more purchases, so all 15 appear.
WITH numbered AS (
SELECT user_id, total_amount, transaction_date, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY transaction_date) AS rn
FROM transactions
)
SELECT user_id, total_amount, transaction_date
FROM numbered
WHERE rn = 2One pass: number each user's purchases by date, then keep rn = 2.
- Collapse per user
GROUP BY user_idwithMIN(transaction_date)gives one row per user, but it is the first purchase. A baretotal_amountnext to the aggregate comes from whichever row the engine picks. Nothing in an aggregate says 'the second one'.- Number per user
ROW_NUMBER()keeps every row and labels its position, sorn = 2selects a real row with its owntotal_amountandtransaction_datealready attached.
SELECT user_id, MIN(transaction_date) AS transaction_date
FROM transactions
GROUP BY user_idReturns each user's first purchase date and no amount: the shape candidates reach for first.
Common follow-up questions
- Now return each user's second purchase and the days elapsed since their first. (Tests reading the neighboring row (`LAG`) in the same pass as the numbering.)
- Two purchases share a `transaction_date` and finance wants the lower `transaction_id` to win. What changes? (Tests adding a deterministic tiebreaker to the ordering.)
- Users with exactly one purchase should now appear with an empty second purchase. How do you change it? (Tests keeping unmatched users: a per-user list outer-joined to the slot-two rows.)
- At 150M rows with a few users holding most purchases, where does this query get slow? (Tests recognizing that a skewed `user_id` puts one huge partition on a single worker.)