The Lion's Share
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
- Allstate
Problem
The merchandising finance team wants to see which product categories bring in the most money. Match every purchase to its product's category, then for each category report its total purchase revenue and how many purchases stood behind it, with the biggest earners listed first.
Every category claims its slice. Find out who really owns the table.
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 | total_revenue | transaction_count |
|---|---|---|
| Electronics | 15016.8 | 20 |
| Books | 12501.39 | 19 |
| Clothing | 12487.92 | 19 |
| Home & Kitchen | 12222 | 18 |
| Toys | 12090.78 | 18 |
Worked solution and explanation
What this really is
This is a fact-to-dimension join with a per-group rollup, dressed up as a merchandising review. The category a purchase belongs to lives only in products, never in transactions, so the first real move is the inner join that lends each purchase its category. After that it is a GROUP BY p.category with SUM(t.total_amount) for revenue and COUNT(*) for volume. The decision that separates a reproducible answer from a flaky one is the ORDER BY: rank by the summed revenue, whose per-category values are all distinct, not by the raw purchase count, where several categories tie on exactly 18 and come back in whatever order the engine feels like.
Break down the requirements
Step 1: Borrow the category from the dimension
transactions has no category column, so each purchase must be matched to its product with a join on product_id against products. An inner join is right here: a purchase with no matching product carries no category to report on, so it drops out rather than forming a null bucket.
Step 2: Collapse to one row per category
GROUP BY p.category folds every purchase in a category into a single output row. SUM(t.total_amount) gives the category's revenue and COUNT(*) gives how many purchases produced it. COUNT(*) is a raw row count, one per transaction, not a count of distinct users or products.
Step 3: Rank by revenue, and make it total
Order by total_revenue descending so the biggest earners surface first. The reason this beats the intuitive COUNT(*) ordering is determinism: several categories tie at exactly 18 purchases, so ranking by the count leaves those rows in engine-arbitrary order, while the per-category revenue sums are all distinct and give one reproducible sequence. Note you order by the SUM, not by a raw total_amount column, which no longer exists after the grouping.
The solution
SELECT p.category AS category, SUM(t.total_amount) AS total_revenue, COUNT(*) AS transaction_count FROM transactions t JOIN products p ON t.product_id = p.product_id GROUP BY p.category ORDER BY total_revenue DESC;Common follow-up questions
- How would you change the query to also include categories that exist in the catalog but have never been purchased, showing them with zero revenue? (Tests switching to a left join from products to transactions and coalescing the NULL sum to zero.)
- The team now wants each category's percentage of total revenue alongside the totals. How would you add that in one pass? (Tests computing a part-to-whole share in a single pass with an aggregate-of-an-aggregate window, `SUM(SUM(total_amount)) OVER ()`, rather than a second scan of the table.)