Category Buyers
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
A merchandising analyst noticed the promotion shortlist keeps surfacing categories that fewer than three different customers have ever bought from. Using transactions and products, list the categories that clear that bar, with each one's number of unique buyers and total revenue, most revenue first.
Which categories have the broadest reach?
Tables
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 |
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 |
Expected output
| category | unique_buyers | total_revenue |
|---|---|---|
| Electronics | 3 | 15016.8 |
| Books | 3 | 12501.39 |
| Clothing | 3 | 12487.92 |
| Home & Kitchen | 3 | 12222 |
| Toys | 3 | 12090.78 |
Worked solution and explanation
A customer count dressed up as a sales report
Anyone can join transactions to products and sum total_amount per category. What separates candidates is counting the audience as different user_id values rather than purchase rows, then putting the at-least-three cutoff on that per-category count. On this data every category has about twenty purchases but only three buyers, so COUNT(*) reports roughly twenty everywhere, and 'Collectibles', which only two customers ever bought from, sails onto the shortlist it should miss.
Step 1: Attach a category to every sale
Inner-join transactions to products on product_id. A few sales carry a NULL product_id and cannot be placed in any category; the inner join drops them, which is right, because revenue you cannot attribute is not a category's revenue.
Step 2: Count people, not rows
COUNT(DISTINCT t.user_id) collapses a customer's repeat purchases into one buyer. Beside it, SUM(t.total_amount) adds every sale as stored, refunds and voids included, because revenue is the money that actually moved.
Step 3: Cut on the category's audience
The cutoff is a property of the category after its sales are rolled up, so it lives in HAVING COUNT(DISTINCT t.user_id) >= 3. A WHERE sees one sale at a time and has no buyer count to compare against.
Step 4: Lead with the money
ORDER BY total_revenue DESC sorts on the alias, so the revenue expression is written once and the biggest categories come first.
SELECT
p.category,
COUNT(DISTINCT t.user_id) AS unique_buyers,
SUM(t.total_amount) AS total_revenue
FROM products p
JOIN transactions t ON p.product_id = t.product_id
GROUP BY p.category
HAVING COUNT(DISTINCT t.user_id) >= 3
ORDER BY total_revenue DESC- Counting purchases
COUNT(*)orCOUNT(t.user_id)counts every sale row: about twenty per category, and 'Collectibles' with four sales clears a three-buyer cutoff it should fail.- Counting customers
COUNT(DISTINCT t.user_id)counts each customer once: three per category, and 'Collectibles' stops at two and drops out.
Common follow-up questions
- How would you keep categories under three buyers but show them at the bottom? (Moves the cutoff out of `HAVING` into a flag column or a `CASE` in the sort.)
- What if you also needed the average sale size per category? (Adds `AVG(t.total_amount)` alongside the other aggregates in the same rollup.)
- How does this behave with 10M products instead of 8K? (The product side no longer fits comfortably in memory, so the join turns into a partitioned shuffle join.)