Going Once
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
- Asked in
- TikTok
Problem
The product team is building a sales leaderboard for in-stock items. For each product currently in stock, show how many transactions reference it, the highest transaction amount, and which user placed the top transaction; products with no transactions should still appear, showing a count of zero and no top user. Walk the catalog in product-ID order, from the lowest number up.
The hammer falls. Who bid the most?
Tables
products
Columns: product_id INTEGER, product_name TEXT, in_stock INTEGER, category TEXT, price REAL, rating REAL
| 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, product_id INTEGER, total_amount REAL, user_id INTEGER, quantity INTEGER, 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
| product_id | product_name | bid_count | highest_bid | winner |
|---|---|---|---|---|
| 1001 | Basic Device 1X | 3 | 225.51 | 197 |
| 1050 | Deluxe Bundle 2X | 3 | 238.98 | 294 |
| 1099 | Pro Unit 3X | 2 | 50.4 | 391 |
| 1491 | Basic Device 11X | 0 | NULL | NULL |
| 1736 | Classic System 16X | 1 | 225.51 | 197 |
Worked solution and explanation
What this problem is really about
This is 'fetch the row that owns the aggregate' wearing an auction costume. Anyone can hand you MAX(total_amount) per product with a GROUP BY. The skill being probed is returning WHO placed that top transaction in the same pass: MAX gives you the number, not the user_id attached to it. A correlated subquery or self-join gets the winner, but it re-scans the entire transactions table once per product. The clean move is ROW_NUMBER() in a CTE to tag each product's top bid once, then an outer join back to it. And there is a trap in the empties: filter the products with an inner join and every in-stock listing that never sold silently disappears; the count has to survive as 0 with a NULL winner.
Break down the requirements
Step 1: Rank each product's bids
In ranked_bids, rank transactions within each product_id by total_amount descending, with transaction_id as the tie-breaker for determinism, using ROW_NUMBER().
Step 2: Outer-join so empty products survive
LEFT JOIN the in-stock products to transactions so products with no transactions are retained, and to ranked_bids restricted to rn = 1 to surface the top-transaction user.
Step 3: Aggregate to one row per product
Aggregate per product: COUNT(t.transaction_id) gives 0 for transaction-less products, MAX(t.total_amount) gives the highest bid, and the rn = 1 user_id is the winner (NULL when no transactions).
Step 4: Order by product id
Order by product_id, lowest first. The key is unique per row, so the order is total.
The solution
WITH ranked_bids AS (
SELECT
product_id,
user_id,
ROW_NUMBER() OVER (PARTITION BY product_id ORDER BY total_amount DESC, transaction_id) AS rn
FROM transactions
)
SELECT
p.product_id,
p.product_name,
COUNT(t.transaction_id) AS bid_count,
MAX(t.total_amount) AS highest_bid,
rb.user_id AS winner
FROM products p
LEFT JOIN transactions t ON p.product_id = t.product_id
LEFT JOIN ranked_bids rb ON p.product_id = rb.product_id AND rb.rn = 1
WHERE p.in_stock = 1
GROUP BY p.product_id, p.product_name, rb.user_id
ORDER BY p.product_id;Common follow-up questions
- If two users place the exact same highest bid on a product, which one does your query report as the winner, and how would you make that choice explicit? (Tests understanding of tie semantics between `ROW_NUMBER()`, `RANK()` and `DENSE_RANK()`.)
- Does the database engine materialize your CTE or inline it? How would you check, and when does it matter? (Tests understanding of CTE materialization behavior, which varies by engine (PostgreSQL materializes by default before v12).)
- `transaction_id` is unique for every bid. What index strategy keeps your query from doing a full table scan as the bid volume grows? (Tests whether the candidate can design indexes for high-cardinality columns and understands selectivity.)
- If the business definition of `in_stock` changed mid-quarter (e.g., a status value was renamed), how would you handle historical consistency? (Tests awareness of slowly changing dimensions and backward-compatible query design.)