Top Spender
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
- L4
Problem
The VIP team is assembling a shortlist of the platform's biggest spenders on well-reviewed products. Total each customer's spending on products rated four stars or better, and keep only those whose total tops the largest single order ever placed on the platform. Return the username and that total, biggest spenders first.
When your spending exceeds the priciest item on the shelf.
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 |
products
Columns: product_id INTEGER, product_name TEXT, category TEXT, price REAL, rating REAL, in_stock INTEGER
| product_id | product_name | category | price | rating | in_stock |
|---|---|---|---|---|---|
| 1001 | Basic Device 1X | Books | 17.52 | 2.7 | 1 |
| 1050 | Deluxe Bundle 2X | Clothing | 25.05 | 4.4 | 1 |
| 1099 | Pro Unit 3X | Home & Kitchen | 32.58 | 2.1 | 1 |
| 1148 | Ultra Tool 4X | Collectibles | 40.11 | 3.8 | 1 |
| 1197 | Essential Set 5X | Toys | 47.64 | 1.5 | 1 |
| 1246 | Classic System 6X | Beauty | 0 | 3.2 | 1 |
| 1295 | Smart Gadget 7X | Automotive | 62.7 | 4.9 | 0 |
| 1344 | Eco Kit 8X | Garden | 70.23 | 2.6 | 1 |
| 1393 | Turbo Pack 9X | Music | 77.76 | 4.3 | 0 |
| 1442 | Premium Widget 10X | Electronics | 85.29 | 2 | 1 |
Expected output
| username | total_spend |
|---|---|
| ava99 | 3892.38 |
| bilal | 3515.2200000000003 |
| anika | 3191.9399999999996 |
| alice | 2868.66 |
| bryce | 2814.7799999999997 |
Worked solution and explanation
What this problem is really testing
Strip off the VIP costume and this is a two-scope comparison: a per-customer sum held against one number computed over the whole platform. Everyone writes the three-table join and the four-star filter. What separates candidates is keeping the two scopes apart. The rating cut decides which orders feed each total. The bar is the largest order across every row of transactions, and the test runs only once the sum exists. Test individual orders against the bar and the list comes back empty, because no order beats the largest order. Take the bar from four-star orders only and it drops: the biggest order on this data is for a product rated 3.0, so a filtered bar lets in customers who never cleared the real one.
Working through it
Step 1: Wire up the three tables
Join users to transactions on user_id, and transactions to products on product_id, so each order carries its buyer's username and its product's rating. Inner joins are right here: a customer with no four-star order has nothing to total and stays off the list. Orders with a missing product_id drop out too, since nobody can say how their product was rated.
Step 2: Filter orders, not customers
p.rating >= 4 is a fact about one order's product, so it goes in WHERE, before anything is summed. Products with a NULL rating fail the comparison and fall out. That is what you want: an unrated product is not well-reviewed.
Step 3: Sum per customer, then gate the total
Group on u.user_id and u.username, sum t.total_amount into total_spend, and only then compare: HAVING SUM(t.total_amount) > (SELECT MAX(total_amount) FROM transactions). The subquery reads the whole table, untouched by the rating filter, and returns one number. Grouping on the id rather than the name means two customers who share a name are never merged.
Step 4: Put the biggest spenders on top
ORDER BY total_spend DESC is the whole order contract. The grade checks that your rows descend by total_spend, and customers who tie may come back either way, so you need no tiebreaker.
The solution
SELECT u.username,
SUM(t.total_amount) AS total_spend
FROM users u
JOIN transactions t ON u.user_id = t.user_id
JOIN products p ON t.product_id = p.product_id
WHERE p.rating >= 4
GROUP BY u.user_id, u.username
HAVING SUM(t.total_amount) > (SELECT MAX(total_amount) FROM transactions)
ORDER BY total_spend DESC- Bar over four-star orders
(SELECT MAX(t.total_amount) FROM transactions t JOIN products p ON t.product_id = p.product_id WHERE p.rating >= 4)skips the largest order because its product is rated below four, so it returns a smaller number and admits more customers.- Bar over every order
(SELECT MAX(total_amount) FROM transactions)reads every order, whatever it was for, so the bar is the platform's true largest order and only customers who beat it stay.
Common follow-up questions
- How would the query change if the bar were each customer's own largest order rather than the platform-wide one? (Tests whether they can turn the uncorrelated subquery into a per-customer comparison and see what it does to the plan.)
- What if refunds sit in `transactions` as negative `total_amount` rows? (Tests whether net spend is what the shortlist should measure, and whether refunds should also be excluded from the bar.)
- If the team wanted five-star purchases only, held against twice the largest order, which clauses move? (Probes the `WHERE` versus `HAVING` boundary: the rating cut is a row change, the spend bar is a group change.)