Extreme Category Totals
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
- L5
- Asked in
- PayU
Problem
For this calendar year, we're comparing product categories by their total sales. Combine each transaction with its product to get the category, then return the three categories with the highest totals and the three with the lowest, from most to least.
The highest and the lowest. Both are interesting.
Tables
transactions
Columns: transaction_id INTEGER, product_id INTEGER, total_amount REAL, transaction_date TEXT, user_id INTEGER, quantity INTEGER
| 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, category TEXT, product_name 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 |
|---|---|
| Electronics | 15016.8 |
| Toys | 12090.78 |
| Books | 6363.45 |
| Clothing | 6363.45 |
| Sports | 5835.09 |
Worked solution and explanation
What this problem is really testing
This is a top-three-and-bottom-three in one result, dressed up as a category review. Anyone can sum the money per category. What separates candidates is taking both ends of the same sorted list without building it twice, keeping a category that sits level with third place, and filtering to 2026 without tripping over types. On this data Books and Clothing tie for third at the top. Two LIMIT 3 cuts return one of them, picked arbitrarily, and quietly drop the other.
Build it up
Step 1: Join transactions to products
transactions carries product_id but not category, so inner join to products on product_id to attach it. Sales with no product_id have no category to land in and drop out here, which is right for a per-category view.
Step 2: Compare the year as a number
strftime('%Y', t.transaction_date) returns the year as text, and text never equals the number 2026. Without a conversion the filter matches nothing and every category vanishes, with no error to warn you. CAST(strftime('%Y', t.transaction_date) AS INTEGER) puts integers on both sides of the =.
Step 3: Aggregate once into a CTE
WITH yearly AS (SELECT category, SUM(total_amount) AS total ... GROUP BY category). Build the per-category total once, so the fact table is scanned a single time and everything after works on about ten rows.
Step 4: Take the three highest and three lowest
Reference the CTE against itself. A category belongs at the top when fewer than three categories out-total it: (SELECT COUNT(*) FROM yearly h WHERE h.total > c.total) < 3. Mirror it with l.total < c.total for the bottom. Because the counts are strictly greater or strictly lower, a tie at either cutoff brings every level category along, where a plain LIMIT 3 would cut one off.
The solution
WITH yearly AS (
SELECT
p.category AS category,
SUM(t.total_amount) AS total
FROM transactions t
JOIN products p ON t.product_id = p.product_id
WHERE CAST(strftime('%Y', t.transaction_date) AS INTEGER) = 2026
GROUP BY p.category
)
SELECT c.category, c.total
FROM yearly c
WHERE (SELECT COUNT(*) FROM yearly h WHERE h.total > c.total) < 3
OR (SELECT COUNT(*) FROM yearly l WHERE l.total < c.total) < 3
ORDER BY c.total DESC- UNION of two LIMIT 3 (drops ties)
(SELECT ... ORDER BY total DESC LIMIT 3) UNION ALL (SELECT ... ORDER BY total ASC LIMIT 3). Simple to read, butLIMIT 3cuts at exactly three, so Books or Clothing goes missing. The two halves also need a final sort.- CTE + correlated counts (keeps ties)
- Build
yearlyonce, then keep any category with fewer than three strictly-greater or strictly-lower totals. It is the same single scan of the fact table, and a boundary tie brings every level category along.
COMMON FOLLOW-UP QUESTIONS
Common follow-up questions
- Rewrite the year filter to be sargable on the partition key. (Tests whether you know that extracting the year with `strftime` blocks partition pruning, and that a half-open range on `transaction_date` is the fix.)
- Return categories with zero sales this year as part of the low end. (Pushes you to start from `products` with a `LEFT JOIN` and wrap the sum in `COALESCE`, so empty categories land at the bottom.)
- The business wants exactly three rows per side even when totals tie. What changes? (Probes the difference between a counting cutoff and row numbering at the boundary, and how each treats ties.)
- How would you extend this to top five and bottom five without scanning the fact table twice? (Checks that the cutoff is a parameter on the single `yearly` build, not a second pass.)