Big Spenders

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

Refunds and zero-dollar rows in transactions are not purchases, and our loyalty tier only opens to customers with at least 13 real purchases. For each customer who qualifies, report their total spend and purchase count, biggest spenders first.

The whale list.

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 10 rows
user_idlifetime_spendtx_count
107010511.7614
97310191.9613
87610134.614
7799946.0214
5859491.5213

Worked solution and explanation

This is a two-level filter dressed up as a loyalty program. One rule is about rows: a refund or a zero-dollar line is not a purchase. The other is about people: a customer earns the tier with at least 13 real purchases. Anyone can write the GROUP BY. What separates candidates is putting each rule on the level it lives on: drop the bad rows before grouping, judge customers after. Skip the row cleanup and a refund quietly counts as a purchase, pushing someone over the bar who never earned it while dragging their spend total down.

Two filters, two levels

Step 1: Throw out the non-purchases

WHERE total_amount > 0 removes the negative refund row and the zero-dollar row before anything is counted. It has to happen first: both the purchase count and the spend should only ever see real purchases.

Step 2: Roll up per customer

GROUP BY user_id, emitting SUM(total_amount) AS lifetime_spend and COUNT(*) AS tx_count. total_amount is already the line total, so it is summed as-is, never multiplied by quantity.

Step 3: Judge the customer, not the row

HAVING COUNT(*) >= 13. A purchase count does not exist until rows are grouped, so this test cannot live in WHERE. It runs on the cleaned count, which is exactly why step 1 had to come first.

Step 4: Biggest spenders first

ORDER BY lifetime_spend DESC. The sort key is a returned column, so the grader checks the order by spend and nothing else.

Clean rows, then judge customers
SELECT user_id,
       SUM(total_amount) AS lifetime_spend,
       COUNT(*) AS tx_count
FROM transactions
WHERE total_amount > 0
GROUP BY user_id
HAVING COUNT(*) >= 13
ORDER BY lifetime_spend DESC
Count first, ask later
Grouping raw rows means COUNT(*) includes the refund and the zero-dollar row. A customer with 12 real purchases plus a refund reads as 13 and gets in, and the refund shaves their lifetime_spend.
Clean first, then count
WHERE total_amount > 0 runs before the rollup, so COUNT(*) and SUM(total_amount) both describe real purchases, and HAVING judges each customer on the honest number.

Where the interviewer goes next

Common follow-up questions