Above the Curve
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 growth team is building a high-value customer segment for an upcoming campaign. Compute each user's total spend, then surface only the users who land above the average customer's spend, showing their ID, total, and how far above that benchmark they sit. List the biggest spenders first.
Spenders who break from the pack.
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 | above_avg |
|---|---|---|
| 1070 | 10511.76 | 1325.6640000000007 |
| 973 | 10146.96 | 960.8639999999996 |
| 876 | 10134.6 | 948.5040000000008 |
| 779 | 9946.02 | 759.9240000000009 |
| 682 | 9757.44 | 571.344000000001 |
Worked solution and explanation
Why this problem exists in real interviews
This is a grain-mismatch trap wearing a marketing costume. Anyone can group and sum; what separates candidates is where they measure the average. Compute AVG(total_amount) over transaction rows and you get the size of a typical purchase, a number far below what a typical customer spends over all their purchases, so nearly everyone clears the bar and the segment stops being high-value. The average has to be taken over the per-user totals, and HAVING alone cannot reach that overall number, which is exactly why the CTE plus scalar subquery is load-bearing here.
Break down the requirements
Step 1: Roll up to user grain
Aggregate transactions with GROUP BY user_id and SUM(total_amount). This is the unit the prompt cares about, not raw rows.
Step 2: Define the threshold
The cutoff is AVG(total) over the per-user totals, not AVG(total_amount) over transactions. Put the rollup in a CTE so the average runs against the right grain.
Step 3: Filter and project the delta
Keep users where total > (SELECT AVG(total) FROM totals), then project total - that_average as above_avg. The scalar subquery runs once.
Step 4: Sort biggest first
ORDER BY total DESC so the campaign team sees the whales at the top.
The solution
WITH totals AS (
SELECT user_id, SUM(total_amount) AS total
FROM transactions
GROUP BY user_id
)
SELECT
user_id,
total,
total - (SELECT AVG(total) FROM totals) AS above_avg
FROM totals
WHERE total > (SELECT AVG(total) FROM totals)
ORDER BY total DESCCOMMON FOLLOW-UP QUESTIONS
Common follow-up questions
- How would you rewrite this using a window function instead of a scalar subquery? (Probes whether you can swap `(SELECT AVG(total) FROM totals)` for `AVG(total) OVER ()` and reason about the tradeoff.)
- What changes if we want users above the 90th percentile instead of the mean? (Tests `PERCENTILE_CONT` or `NTILE(10)` and whether you understand mean versus quantile thresholds.)
- How do you make this incremental over a rolling 30 day window? (Pushes you on `WHERE transaction_date >= CURRENT_DATE - INTERVAL '30 day'` and partition pruning on the 50M row table.)
- What if `total_amount` can be negative due to refunds? (Surfaces whether you net them, exclude them, or treat refunds as a separate signal.)