Cart Sizes
An easy SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.
- Domain
- SQL
- Difficulty
- easy
- Seniority
- L3
Problem
An analyst noticed the bulk-checkout invite list was counting refunds and zero-value adjustments in transactions as purchases, which pushed the wrong buyers over the bar. Rebuild it from purchases with a positive amount only: every user in users with more than twelve of them, showing their username, purchase count and total spent, biggest spenders first.
Power buyers. Big carts.
Tables
users
Columns: user_id INTEGER, username TEXT, email TEXT, signup_date TEXT, account_status TEXT, age_bucket TEXT
| user_id | username | signup_date | account_status | age_bucket | |
|---|---|---|---|---|---|
| 100 | alice | alice@example.com | 2025-02-02 | inactive | 25-34 |
| 197 | aaron42 | aaron42@example.com | 2026-03-03 | suspended | 35-44 |
| 294 | amelia | amelia@example.com | 2024-04-04 | pending_verification | 45-54 |
| 391 | arjun | arjun@example.com | 2025-05-05 | active | 55-64 |
| 488 | ava99 | ava99@example.com | 2026-06-06 | inactive | 65+ |
| 585 | andrew_k | andrew_k@example.com | 2024-07-07 | suspended | NULL |
| 682 | anika | anika@example.com | 2025-08-08 16:34:00 | pending_verification | 18-24 |
| 779 | aiden | aiden@example.com | 2026-09-09 14:53:00 | active | 25-34 |
| 876 | aria_b | aria_b@example.com | 2024-10-10 | inactive | 35-44 |
| 973 | brian | brian@example.com | 2025-11-11 | suspended | 45-54 |
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
| username | purchase_count | total_spent |
|---|---|---|
| bjorn99 | 14 | 10511.76 |
| brian | 13 | 10191.96 |
| aria_b | 14 | 10134.6 |
| aiden | 14 | 9946.02 |
| andrew_k | 13 | 9491.52 |
Worked solution and explanation
What this really is
Under the invite-list story this is two filters living at two different grains. The positive-amount rule decides which ledger rows are purchases at all; the more-than-twelve bar decides which users survive once those purchases are counted. Anyone can write the join and the count. What separates candidates is putting each filter where its data lives. Every buyer on this data sits at twelve or fourteen ledger rows, so the bar is right on the edge: let a refund row into the count and a buyer can clear it on a purchase they gave back.
Build it in order
Step 1: Attribute each purchase to a buyer
Inner-join users to transactions on user_id. Users with no purchases can never reach thirteen, so the inner join loses nothing the answer needs.
Step 2: Throw out non-purchases before counting
WHERE t.total_amount > 0 is a row condition: it looks at one ledger row at a time, so it runs before grouping and keeps the zero-value row and the refund out of both COUNT(*) and SUM(t.total_amount).
Step 3: Count and total per user_id
GROUP BY u.user_id, u.username keys the group on the primary key and carries the display name along, so two people who share a username never merge.
Step 4: Cut on the count, then sort by spend
HAVING COUNT(*) > 12 filters groups after they are counted, and ORDER BY total_spent DESC puts the biggest spenders on top. The count decides who is on the list; the spend decides where they sit.
SELECT u.username,
COUNT(*) AS purchase_count,
SUM(t.total_amount) AS total_spent
FROM users AS u
JOIN transactions AS t
ON t.user_id = u.user_id
WHERE t.total_amount > 0
GROUP BY u.user_id, u.username
HAVING COUNT(*) > 12
ORDER BY total_spent DESC- Row filter
WHEREsees one ledger row at a time. It can testtotal_amount, but no per-user count exists yet, soWHERE COUNT(*) > 12is rejected outright.- Group filter
HAVINGsees one user at a time, after counting. It can testCOUNT(*), but by then the individualtotal_amountvalues are gone, so the refund rule cannot live here.
Common follow-up questions
- What if refunds should cancel a purchase rather than just be ignored? (Tests netting `total_amount` per user versus dropping rows.)
- How would you show only the top 10 spenders, and what decides who makes the cut on a tie at 10th? (Tests `LIMIT` and when a tie rule becomes necessary.)
- What changes if power buyers are judged only on the last twelve months? (Tests adding a second row condition on `transaction_date` without touching the group filter.)