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

users: 10 of 200 rows
user_idusernameemailsignup_dateaccount_statusage_bucket
100alicealice@example.com2025-02-02inactive25-34
197aaron42aaron42@example.com2026-03-03suspended35-44
294 ameliaamelia@example.com2024-04-04pending_verification45-54
391arjun arjun@example.com2025-05-05active55-64
488ava99ava99@example.com2026-06-06inactive65+
585andrew_kandrew_k@example.com2024-07-07suspendedNULL
682anikaanika@example.com2025-08-08 16:34:00pending_verification18-24
779aidenaiden@example.com2026-09-09 14:53:00active25-34
876aria_baria_b@example.com2024-10-10inactive35-44
973brianbrian@example.com2025-11-11suspended45-54

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

products

Columns: product_id INTEGER, product_name TEXT, category TEXT, price REAL, rating REAL, in_stock INTEGER

products: 10 of 200 rows
product_idproduct_namecategorypriceratingin_stock
1001Basic Device 1XBooks17.522.71
1050Deluxe Bundle 2XClothing25.054.41
1099Pro Unit 3XHome & Kitchen32.582.11
1148Ultra Tool 4XCollectibles40.113.81
1197Essential Set 5XToys47.641.51
1246Classic System 6XBeauty03.21
1295Smart Gadget 7XAutomotive62.74.90
1344Eco Kit 8XGarden70.232.61
1393Turbo Pack 9XMusic77.764.30
1442Premium Widget 10XElectronics85.2921

Expected output

First 5 of 13 rows
usernametotal_spend
ava993892.38
bilal3515.2200000000003
anika3191.9399999999996
alice2868.66
bryce2814.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

Filter orders in `WHERE`, gate the total in `HAVING`
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