The Standings
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
Problem
In these standings, categories that moved the same number of units share a standing, and the next category's standing still counts every category above it. A merchandising analyst needs every category's total revenue and total units from transactions, each sale's category coming from products, along with its standing by units with the biggest mover in first place.
Every category earns its place on the board. Find where each one lands.
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 | total_revenue | total_units | position |
|---|---|---|---|
| Music | 11884.14 | 90 | 1 |
| Home & Kitchen | 12222 | 72 | 2 |
| Garden | 12087.3 | 72 | 2 |
| Sports | 11670.18 | 70 | 4 |
| Clothing | 12487.92 | 57 | 5 |
Worked solution and explanation
What this problem is really about
Under the merchandising framing this comes down to one question: when two categories move the same number of units, what standing does the next one get? Anyone can join, sum and sort. The skill is numbering standings so that a tie shares a place and the place after it still counts every category above. On this data that matters twice: 'Home & Kitchen' and 'Garden' both move 72 units, and 'Electronics' and 'Collectibles' both move 20. ROW_NUMBER() splits each pair. DENSE_RANK() keeps each pair together but closes the gap, so 'Sports' reads 3 instead of 4 and 'Toys' reads 9 instead of 11. In both cases the standings look plausible and are wrong from the third row down.
Build it in three moves
Step 1: Join transactions to products
Join on product_id to give each sale its category. Make it an inner join: 18 sales carry no product_id, and an outer join from transactions would turn them into a twelfth row with a NULL category.
Step 2: Total each category
SUM(t.total_amount) is the revenue and SUM(t.quantity) is the units, grouped by p.category. The refund and the voided sale stay in, because they are recorded sales and the totals are what was recorded.
Step 3: Place categories by units
RANK() OVER (ORDER BY SUM(t.quantity) DESC) reads the grouped total directly, because window functions run after GROUP BY. The descending direction puts the biggest mover in place 1.
The solution
SELECT
p.category,
SUM(t.total_amount) AS total_revenue,
SUM(t.quantity) AS total_units,
RANK() OVER (ORDER BY SUM(t.quantity) DESC) AS position
FROM products p
JOIN transactions t ON p.product_id = t.product_id
GROUP BY p.category- Gapless numbering
DENSE_RANK()by units: 'Music' 1, 'Home & Kitchen' 2, 'Garden' 2, 'Sports' 3, then down to 'Electronics' 8, 'Collectibles' 8, 'Toys' 9.- Standings
RANK()by units: 'Music' 1, 'Home & Kitchen' 2, 'Garden' 2, 'Sports' 4, then down to 'Electronics' 9, 'Collectibles' 9, 'Toys' 11.
Common follow-up questions
- What is the difference between `RANK()` and `DENSE_RANK()` on this data? (`RANK()` gives 1, 2, 2, 4 and `DENSE_RANK()` gives 1, 2, 2, 3. This tests whether the candidate can say which one standings need.)
- What if categories with no sales had to appear with zero units? (This tests a `LEFT JOIN` from `products` with `COALESCE` on the sums, and where zero lands in the standings.)
- How would you show only the top three standings, ties included? (Wrap the query and filter on `position <= 3`. With the tie at 2, that returns three categories.)