Median Transaction by Category
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
- L4
Problem
Compute the median transaction amount for each product category. Show each category alongside its median, walking the categories in alphabetical order.
The middle transaction in each category.
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 |
products
Columns: product_id INTEGER, product_name TEXT, category TEXT, price REAL, rating REAL, in_stock INTEGER
| product_id | product_name | category | price | rating | in_stock |
|---|---|---|---|---|---|
| 1001 | Basic Device 1X | Books | 17.52 | 2.7 | 1 |
| 1050 | Deluxe Bundle 2X | Clothing | 25.05 | 4.4 | 1 |
| 1099 | Pro Unit 3X | Home & Kitchen | 32.58 | 2.1 | 1 |
| 1148 | Ultra Tool 4X | Collectibles | 40.11 | 3.8 | 1 |
| 1197 | Essential Set 5X | Toys | 47.64 | 1.5 | 1 |
| 1246 | Classic System 6X | Beauty | 0 | 3.2 | 1 |
| 1295 | Smart Gadget 7X | Automotive | 62.7 | 4.9 | 0 |
| 1344 | Eco Kit 8X | Garden | 70.23 | 2.6 | 1 |
| 1393 | Turbo Pack 9X | Music | 77.76 | 4.3 | 0 |
| 1442 | Premium Widget 10X | Electronics | 85.29 | 2 | 1 |
Expected output
| category | median_amount |
|---|---|
| Automotive | 643.08 |
| Beauty | 629.61 |
| Books | 696.96 |
| Clothing | 710.43 |
| Collectibles | 131.22 |
Worked solution and explanation
What this problem really is
This is an exact per-group median dressed up as a dashboard metric. There is no one-call median here, so you have to find the middle of each category by hand: number every categorized sale from cheapest to dearest inside its category, count the category, and keep the middle position. Most candidates get that far. The even-sized category is what separates them. There is no single middle row there, and a candidate who picks one of the two central sales reports a median off by half the gap between them. The second, quieter trap is the join: sales with no product_id belong to no category and must drop out before anything is counted.
Build it in three moves
Step 1: Attach the category before numbering anything
The category lives on products, so inner-join transactions to products on product_id first. The window can only partition by p.category once every sale carries its category, and the same inner join silently drops the sales whose product_id is empty.
Step 2: Number each sale and size its category
ROW_NUMBER() OVER (PARTITION BY p.category ORDER BY t.total_amount) gives each sale its position from the cheapest up, and COUNT(*) OVER (PARTITION BY p.category) stamps the category size on every row without collapsing them. Both windows share one partition, so each category is sorted once.
Step 3: Keep the middle one or two rows, then average
With integer division, (cnt + 1) / 2 and (cnt + 2) / 2 land on the same position when cnt is odd and on the two central positions when it is even. Filter on that pair, then take AVG(total_amount) per category: one row averages to itself, and two rows average to the true median.
WITH ranked AS (
SELECT p.category, t.total_amount,
ROW_NUMBER() OVER (PARTITION BY p.category ORDER BY t.total_amount) AS rn,
COUNT(*) OVER (PARTITION BY p.category) AS cnt
FROM transactions t INNER JOIN products p ON t.product_id = p.product_id
)
SELECT category, AVG(total_amount) AS median_amount
FROM ranked
WHERE rn IN ((cnt + 1) / 2, (cnt + 2) / 2)
GROUP BY category
ORDER BY category- Picks one middle row
WHERE rn = (cnt + 1) / 2is exact on odd categories. On an even one it returns the lower central sale, somedian_amountsits below the real median by half the gap to its neighbour.- Averages the middle pair
WHERE rn IN ((cnt + 1) / 2, (cnt + 2) / 2)keeps one row or two, and the outerAVGturns either case into the right answer with no branching on parity.
Common follow-up questions
- Why is a median of medians not the true median by category? (Tests statistical literacy: aggregating twice loses the underlying distribution, and one product's outliers can hide inside its own median.)
- How would you weight the median by `quantity` instead of counting each sale once? (Tests whether the position logic generalizes: replace the row number with a running sum of `quantity` and keep the row where it crosses half the category total.)
- What changes if some transactions have a `NULL` `total_amount`? (Tests data hygiene: nulls sort first ascending here and still count toward `cnt`, so they must be filtered out inside the CTE before numbering.)