Across the Aisles
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
Which of our shoppers spread their purchases under $500 across more than one product category? Working from our transactions and each product's category, list them with how many categories they bought from, most categories first.
The best customers never stay in one aisle.
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
| user_id | category_count |
|---|---|
| 488 | 3 |
| 1458 | 2 |
| 1361 | 2 |
| 1264 | 2 |
| 1070 | 2 |
Worked solution and explanation
What this really tests
Under the marketing costume this is a per-shopper distinct count over a filtered slice, with a threshold on the count. Two decisions carry it. The $500 cut is a rule about each purchase, so it decides which rows exist before any shopper is summarized. The segment is about variety, so the number that matters is how many different categories survive the cut, not how many purchases. Count rows and a shopper whose only small buys are two orders from one aisle sails past 'more than one'. Drop the cut and every full history comes back, readmitting exactly the people the segment was built to exclude.
Building it
Step 1: Keep only purchases under $500
WHERE t.total_amount < 500 runs before grouping, which is right: the limit is a property of each purchase, not of the shopper. Once rows are grouped there is no single amount left to compare.
Step 2: Attach the category with an inner join
Join transactions to products on product_id. A purchase with a NULL product_id matches nothing and falls away: no product, no category.
Step 3: Count different categories per user
Group by t.user_id and compute COUNT(DISTINCT p.category). Repeat buys in one aisle collapse to one, so two small book orders still read as a single category.
Step 4: Threshold the count with HAVING
The count does not exist until rows are grouped, so HAVING COUNT(DISTINCT p.category) > 1 is the only place the rule can live. On this data it removes the shoppers whose small purchases all landed in one category.
Step 5: Sort widest reach first
ORDER BY category_count DESC. Shoppers tied on the count may come in any order, so no tiebreaker is owed.
The solution
SELECT t.user_id, COUNT(DISTINCT p.category) AS category_count
FROM transactions t
JOIN products p ON t.product_id = p.product_id
WHERE t.total_amount < 500
GROUP BY t.user_id
HAVING COUNT(DISTINCT p.category) > 1
ORDER BY category_count DESC- Counts purchases
HAVING COUNT(*) > 1orCOUNT(p.category) > 1passes anyone with two small orders, even two from the same aisle, and reports purchase volume ascategory_count.- Counts categories
COUNT(DISTINCT p.category)in both theSELECTand theHAVINGmeasures variety, so repeat buys count once and only real multi-category shoppers survive.
Common follow-up questions
- Restrict the segment to shoppers with at least three categories who also bought from 'Electronics' under $500. What changes? (Tests combining a distinct count with a conditional aggregate in `HAVING`.)
- What if the $500 limit applied to each shopper's total spend instead of each purchase? (Tests moving a condition from `WHERE` to `HAVING` and explaining why the answers differ.)
- How would you also list shoppers with no qualifying purchases, showing zero categories? (Tests an outer join from `users` and a count that handles `NULL`.)