Tiers of Want

A hard SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.

Domain
SQL
Difficulty
hard
Seniority
L5
Asked in
HealthTap

Problem

A retail marketplace wants to see how its shoppers stack up inside each product category, scoring every shopper on their average basket size (total spend divided by number of purchases) in that category. Sort each shopper into a spend tier: High above $500, Medium from $200 up to $500, and Low below that. For each category and tier, report how many shoppers land there along with their combined transactions, combined sales, and the average basket size across them.

In each category, some carts weigh more than others. Sort the shoppers by what they spend.

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

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 11 rows
categorysegmentunique_userstotal_transactionstotal_salesavg_basket_size
AutomotiveHigh31711875.26665.5300000000001
BeautyHigh31711915.67701.4499999999999
BooksHigh31912501.39681.9933333333333
ClothingHigh31912487.92695.4633333333333
CollectiblesLow24524.88131.22

Worked solution and explanation

What this problem really is

This is a two-level rollup dressed up as customer segmentation. The tier belongs to a shopper inside a category, and that shopper's average basket only exists after you collapse transactions to one row per user_id and category. Anyone can write the CASE. What separates candidates is where it runs. Put it on raw total_amount and you tier purchases instead of people, and COUNT(*) then reports transactions where the question wants shoppers. The quieter trap is the last column. SUM(total_sales) / SUM(txn_count) at the tier grain looks like an average basket, but it weights every shopper by how often they buy. On this seed Automotive's High tier is 665.53 as a mean of the shoppers' baskets and 698.54 as a pooled ratio.


Break down the requirements

Step 1: Compute basket size per shopper per category

Join transactions to products for the category, group by user_id, category, and compute SUM(total_amount) * 1.0 / COUNT(*) as basket_size. Every row is one purchase, so COUNT(*) is the right denominator, not SUM(quantity).

Step 2: Label segments on the shopper row

Apply the CASE to that per-shopper average: > 500 is 'High', >= 200 is 'Medium', everything else 'Low'. Because the WHEN clauses are tested in order, the High check must come first.

Step 3: Re-aggregate per category per segment

Group the shopper rows by category, segment. COUNT(*) now counts shoppers, SUM(txn_count) and SUM(total_sales) add up their activity, and AVG(basket_size) gives each shopper one equal vote.


The solution

Two-stage aggregation with segment labeling
WITH user_baskets AS (
    SELECT
        t.user_id,
        p.category,
        SUM(t.total_amount) AS total_sales,
        COUNT(*) AS txn_count,
        SUM(t.total_amount) * 1.0 / COUNT(*) AS basket_size,
        CASE
            WHEN SUM(t.total_amount) * 1.0 / COUNT(*) > 500 THEN 'High'
            WHEN SUM(t.total_amount) * 1.0 / COUNT(*) >= 200 THEN 'Medium'
            ELSE 'Low'
        END AS segment
    FROM transactions t
    JOIN products p ON t.product_id = p.product_id
    GROUP BY t.user_id, p.category
)
SELECT
    category,
    segment,
    COUNT(*) AS unique_users,
    SUM(txn_count) AS total_transactions,
    SUM(total_sales) AS total_sales,
    AVG(basket_size) AS avg_basket_size
FROM user_baskets
GROUP BY category, segment
Pooled ratio
SUM(total_sales) / SUM(txn_count) at the tier grain. A shopper with eight purchases pulls the figure twice as hard as one with four, so frequent buyers decide the number. Automotive High reads 698.54.
Mean of shopper baskets
AVG(basket_size) over the user_baskets rows. Each shopper contributes their own average once, which is what 'the average basket size across them' asks for. Automotive High reads 665.53.

Common follow-up questions