Spending Tiers

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

Label each customer 'high', 'medium' or 'low' by comparing their average transaction amount in our transactions table with the average across every transaction, which is how we bucket our loyalty program. A customer is 'high' only when their average runs more than 5 percent above that overall figure and 'low' only when it runs more than 5 percent below; everyone else is 'medium'.

High rollers, mid-spenders, and the frugal. Everyone gets a tier.

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 15 rows
user_idtier
100medium
197low
294low
391medium
876high

Worked solution and explanation

What this problem is really asking

Strip the loyalty-program costume and this is relative benchmarking at two grains: each customer's own average order size measured against one population-wide average. Anyone can write AVG(total_amount) per user_id. What separates candidates is seeing that the tier boundary is itself a computed number, which has to arrive as a single value and get multiplied into bands. On this data every customer's average sits within about 9 percent of the overall figure near 689, so a hardcoded dollar cutoff drops everyone into one bucket, and tiering raw transactions hands one customer several contradictory labels.


Walking through it

Step 1: Collapse transactions to one average per user_id

Group transactions by user_id and take AVG(total_amount) as avg_spend. This is the customer grain: one row per customer carrying their typical order size, whether they placed twelve orders or fourteen. Averaging here, not summing, is what makes customers with different order counts comparable.

Step 2: Compute the benchmark as a single value

The tier line is the average over every transaction, one number. The scalar subquery (SELECT AVG(total_amount) FROM transactions) delivers it, so each customer is measured against the same population baseline. Refunds and voided sales stay in, exactly as they stay in each customer's own average.

Step 3: Bucket with a CASE over the bands

Multiply the benchmark by 1.05 and 0.95 to draw the bands, then a three-branch CASE labels each customer. Test high first, low next, and let everything else fall through to 'medium', so a customer exactly on either line stays 'medium'. On the seed that splits 15 customers into 3 'high', 2 'low' and 10 'medium'.


The solution

Relative spending tiers
WITH user_avg AS (
    SELECT user_id, AVG(total_amount) AS avg_spend
    FROM transactions
    GROUP BY user_id
)
SELECT
    user_id,
    CASE
        WHEN avg_spend > (SELECT AVG(total_amount) FROM transactions) * 1.05 THEN 'high'
        WHEN avg_spend < (SELECT AVG(total_amount) FROM transactions) * 0.95 THEN 'low'
        ELSE 'medium'
    END AS tier
FROM user_avg

Common follow-up questions