First Among Many
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
- Asked in
- Meta
Problem
We group users by account status, and every segment leans toward one product category more than the rest. For each status, surface the category with the most purchases, where a purchase is a single transaction regardless of how many units it moved. Walk through the segments from the lowest account status upward, and when a status has two categories tied for the lead, let the one that comes first alphabetically take the spot.
Each segment has a favorite category.
Tables
users
Columns: user_id INTEGER, username TEXT, email TEXT, signup_date TEXT, account_status TEXT, age_bucket TEXT
| user_id | username | signup_date | account_status | age_bucket | |
|---|---|---|---|---|---|
| 100 | alice | alice@example.com | 2025-02-02 | inactive | 25-34 |
| 197 | aaron42 | aaron42@example.com | 2026-03-03 | suspended | 35-44 |
| 294 | amelia | amelia@example.com | 2024-04-04 | pending_verification | 45-54 |
| 391 | arjun | arjun@example.com | 2025-05-05 | active | 55-64 |
| 488 | ava99 | ava99@example.com | 2026-06-06 | inactive | 65+ |
| 585 | andrew_k | andrew_k@example.com | 2024-07-07 | suspended | NULL |
| 682 | anika | anika@example.com | 2025-08-08 16:34:00 | pending_verification | 18-24 |
| 779 | aiden | aiden@example.com | 2026-09-09 14:53:00 | active | 25-34 |
| 876 | aria_b | aria_b@example.com | 2024-10-10 | inactive | 35-44 |
| 973 | brian | brian@example.com | 2025-11-11 | suspended | 45-54 |
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
| account_status | category | purchase_count |
|---|---|---|
| active | Automotive | 8 |
| inactive | Garden | 8 |
| pending_verification | Clothing | 9 |
| suspended | Books | 9 |
Worked solution and explanation
What this is really asking
Strip the marketing veneer and this is top-per-group with ties kept, where the group is account_status and the winner count is one. Everyone can join three tables and count; the ranking step is what separates candidates. Aggregate to one row per (status, category) first, then rank those groups inside each status. Reach for ROW_NUMBER and you silently pick one winner when two categories tie, dropping legitimate answers; RANK keeps every category that reaches the top count. Over 200M transactions you also have no choice but to aggregate before you rank, so you rank a few hundred groups, not rows.
Break down the requirements
Step 1: Join and count per pair
Join transactions to users (for account_status) and products (for category), then COUNT(*) grouped by (account_status, category). That collapses 200M rows to at most a few hundred groups. Note it is COUNT(*) of rows, not SUM(quantity): a five-unit basket is still one purchase, and weighting by quantity flips winners in any segment with a big-basket outlier.
Step 2: Rank inside each segment
RANK() OVER (PARTITION BY account_status ORDER BY COUNT(*) DESC). Using RANK keeps every tied top category; ROW_NUMBER would collapse a real tie to one arbitrary winner and quietly delete the rest.
Step 3: Filter and order
Keep rk = 1 and ORDER BY account_status, category. The outer sort is by category name, not by count, because all surviving rows already share the top count within their segment.
The solution
WITH ranked AS (
SELECT
u.account_status,
p.category,
COUNT(*) AS purchase_count,
RANK() OVER (
PARTITION BY u.account_status
ORDER BY COUNT(*) DESC
) AS rk
FROM transactions t
JOIN users u ON t.user_id = u.user_id
JOIN products p ON t.product_id = p.product_id
GROUP BY u.account_status, p.category
)
SELECT account_status, category, purchase_count
FROM ranked
WHERE rk = 1
ORDER BY account_status, category;COMMON FOLLOW-UP QUESTIONS
Common follow-up questions
- How would the answer change if you used `DENSE_RANK` instead of RANK? (Same result here because we only keep rk = 1, but the values differ for non-winners; explain when each matters.)
- What if an `account_status` has zero transactions? (It disappears from the output. Discuss a LEFT JOIN from a status dimension if you need every segment present, even empty ones.)
- How would you weight by `total_amount` instead of count? (Swap COUNT(*) for SUM(`t.total_amount`) in both the aggregate and the ORDER BY of the window. Ties become much rarer.)