The Biggest Fish

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
Barclays

Problem

We're planning loyalty outreach around whoever spends the most in each product category, where every purchase in transactions counts toward a customer's total in the category its product carries in products. Going through the categories alphabetically, return each one's top spender with that total; when two customers tie for the top, list the lower user_id first.

In every aisle, one customer outspends the rest. Find them.

Tables

transactions

Columns: transaction_id INTEGER, user_id INTEGER, product_id INTEGER, quantity INTEGER, total_amount REAL, transaction_date TEXT

transactions: 10 of 200 rows
transaction_iduser_idproduct_idquantitytotal_amounttransaction_date
10671971001223.462026-02-02
11342941050336.932026-03-03
12013911099450.42026-04-04
12684881148563.872026-05-05
13355851197002026-06-06
14026821246290.812026-07-07
146977912953104.282026-08-08
153687613444117.752026-09-09
160397313935-452026-01-10
1670107014421144.692026-02-11

products

Columns: product_id INTEGER, product_name TEXT, category TEXT, price REAL, rating REAL, in_stock INTEGER

products: 10 of 200 rows
product_idproduct_namecategorypriceratingin_stock
1001Basic Device 1XBooks17.522.71
1050Deluxe Bundle 2XClothing25.054.41
1099Pro Unit 3XHome & Kitchen32.582.11
1148Ultra Tool 4XCollectibles40.113.81
1197Essential Set 5XToys47.641.51
1246Classic System 6XBeauty03.21
1295Smart Gadget 7XAutomotive62.74.90
1344Eco Kit 8XGarden70.232.61
1393Turbo Pack 9XMusic77.764.30
1442Premium Widget 10XElectronics85.2921

Expected output

First 5 of 11 rows
categoryuser_idtotal_spent
Automotive7795683.44
Beauty11674585.86
Books1975262.39
Clothing2945383.62
Collectibles1458397.14

Worked solution and explanation

What this problem is really testing

Strip the loyalty framing and this is the classic top-1-per-group. The skill being probed is whether you keep two concerns separate: rolling spend up to the (category, customer) grain, and then picking the winner within each category. Anyone can write the SUM. The tell is which window function you reach for, because RANK and ROW_NUMBER give different answers the moment two customers tie at the top, and only one of them is correct here.

`transactions.product_id` resolves to a category via products; everything else is aggregation.
  • transactions: transaction_id int PK, user_id int, product_id int FK, total_amount numeric
  • products: product_id int PK, category text
  • transactions references products

Break down the requirements

Step 1: Attach a category to every transaction

The category lives in products, not transactions, so join on product_id first. This is an inner join on purpose: a transaction whose product has no category row cannot belong to any category leaderboard, so dropping it is correct, not a bug.

Step 2: Roll up to (category, customer) spend

GROUP BY category, user_id and SUM(total_amount). This collapses many transactions per customer into one total per category, which is the grain the question actually cares about. Do this in a CTE so the ranking step reads cleanly.

Step 3: Rank within each category and keep the top

RANK() OVER (PARTITION BY category ORDER BY total_spent DESC) numbers customers per category, biggest spender first. Filter spend_rank = 1. RANK gives every tied top spender the value 1, so a genuine tie surfaces both customers instead of silently discarding one.


The solution

Top spender per category
WITH category_spend AS (
  SELECT p.category AS category,
         t.user_id AS user_id,
         SUM(t.total_amount) AS total_spent
  FROM transactions t
  JOIN products p ON t.product_id = p.product_id
  GROUP BY p.category, t.user_id
),
ranked AS (
  SELECT category,
         user_id,
         total_spent,
         RANK() OVER (PARTITION BY category ORDER BY total_spent DESC) AS spend_rank
  FROM category_spend
)
SELECT category, user_id, total_spent
FROM ranked
WHERE spend_rank = 1
ORDER BY category, user_id
RANK() (correct)
Ties both get rank 1, so a category with two customers at the same top total returns both rows. That matches 'the customer who spends the most', which is a set, not a guarantee of one row.
`ROW_NUMBER`() (subtly wrong)
ROW_NUMBER assigns 1 and 2 to tied customers arbitrarily, so filtering to 1 keeps whichever row the engine happened to order first and drops a real co-leader. It looks right on clean fixtures and hides the bug until a tie appears in production.

Common follow-up questions