Kings of the Aisle
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
- mid
- Asked in
- Visa
Problem
The merchandising team is reviewing which product owned each category this year. For every category, surface the single product with the highest total sales, listed from the biggest earner down.
Every category crowns exactly one winner. Go find them.
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 | product_name | total_revenue |
|---|---|---|
| Electronics | Premium Widget 100X | 2713.98 |
| Toys | Essential Set 95X | 2579.28 |
| Garden | Eco Kit 98X | 1330.05 |
| Automotive | Smart Gadget 97X | 1316.58 |
| Beauty | Classic System 96X | 1303.11 |
Worked solution and explanation
Why this problem exists in real interviews
This is a top-1-per-group problem wearing a merchandising costume, and it is the exact spot Visa-style SQL rounds probe your window-function instinct. Anyone can join transactions to products and sum revenue by category and product. The real question is how you collapse each category down to its single leader. Reach for MAX(total_revenue) in a second aggregation and you get the right number but lose the product_name attached to it, or you re-join and accidentally surface two products when a category has a tie. The clean move is to number the rows inside each category and keep number one.
The schema you are reasoning about
- transactions: transaction_id int PK, product_id int FK, total_amount num, transaction_date date
- products: product_id int PK, product_name text, category text
- transactions references products
Break down the requirements
Step 1: Join to reach the category
category and product_name live on products, the revenue lives on transactions. Inner join on product_id so every sale carries its category label. An inner join is correct here: a transaction with no matching product cannot be attributed and should drop.
Step 2: Roll up to product-in-category revenue
Filter to this year with strftime('%Y', transaction_date) = '2026', then GROUP BY category, product_name and SUM(total_amount). This is the grain the leaderboard actually cares about, one row per product per category.
Step 3: Number within each category and keep the leader
ROW_NUMBER() OVER (PARTITION BY category ORDER BY total_revenue DESC, product_name) stamps the top earner in each category as 1. Filtering rn = 1 gives exactly one product per category, and the product_name rides along untouched, which a bare MAX() would have severed.
The solution
WITH product_revenue AS (
SELECT p.category AS category,
p.product_name AS product_name,
SUM(t.total_amount) AS total_revenue
FROM transactions t
JOIN products p ON t.product_id = p.product_id
WHERE strftime('%Y', t.transaction_date) = '2026'
GROUP BY p.category, p.product_name
),
ranked AS (
SELECT category,
product_name,
total_revenue,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY total_revenue DESC, product_name) AS rn
FROM product_revenue
)
SELECT category, product_name, total_revenue
FROM ranked
WHERE rn = 1
ORDER BY total_revenue DESCCommon follow-up questions
- Now return the top three products per category instead of just the leader. (Tests whether they see rn = 1 generalizes to rn <= 3 with no structural change.)
- How would you handle categories that had zero sales this year but should still appear in the report? (Pushes toward starting from products (or a category dimension) with a LEFT JOIN so empty categories survive.)
- The team wants each product's share of its category's total revenue alongside the leader. (Tests SUM() OVER (PARTITION BY category) as a second window in the same pass.)