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

transactions: 10 of 200 rows
transaction_iduser_idproduct_idquantitytotal_amounttransaction_date
10671971001223.462026-02-02
11342941050336.932026-03-03
12013911099450.42026-04-04
12684881148563.872026-05-05
13355851197002026-06-06
14026821246290.812026-07-07
146977912953104.282026-08-08
153687613444117.752026-09-09
160397313935-452026-01-10
1670107014421144.692026-02-11

Expected output

First 5 of 15 rows
user_idtotal_amounttransaction_date
100616.142026-01-18
197831.662022-01-06
294845.132023-02-07
391858.62024-03-08
48863.872025-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.

Number per user, keep slot two
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 = 2

One pass: number each user's purchases by date, then keep rn = 2.

Collapse per user
GROUP BY user_id with MIN(transaction_date) gives one row per user, but it is the first purchase. A bare total_amount next 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, so rn = 2 selects a real row with its own total_amount and transaction_date already attached.
The aggregate that answers a different question
SELECT user_id, MIN(transaction_date) AS transaction_date
FROM transactions
GROUP BY user_id

Returns each user's first purchase date and no amount: the shape candidates reach for first.

Common follow-up questions