The Spender Leaderboard

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
L3
Asked in
IBM

Problem

A spend leaderboard that stops at exactly five rows can drop a user who is level with the fifth-place spender, and the growth team's rewards review needs nobody left off. From the transactions table, list every user in the top five spending positions with their total spend and position, biggest spenders first. Users with equal totals share a position, and the next total takes the following number.

Spending speaks. The leaderboard does the listening.

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

Expected output

First 5 of 5 rows
user_idtotal_spendrnk
107010511.761
97310146.962
87610134.63
7799946.024
6829757.445

Worked solution and explanation

Five positions, not five rows

This is a top-N by standing dressed up as a leaderboard. Anyone can sum total_amount per user_id. What separates candidates is reading 'top five' as five positions, not five rows. A LIMIT 5 returns five rows whatever the totals do. So the first time two users finish level at fifth place, one of them silently falls off the board, and nothing errors to tell you.

Step 1: Total each user's spend first

Collapse transactions to one row per user_id with SUM(total_amount). A standing belongs to a user's total, not to a purchase. Rank individual transactions and one heavy buyer can take several places on the board. Refunds and voided sales stay in the sum: they lower what the user actually spent.

Step 2: Number the positions with DENSE_RANK()

DENSE_RANK() OVER (ORDER BY SUM(total_amount) DESC) gives equal totals the same number, and the next total takes the very next one. RANK() would skip after a tie (1, 2, 2, 4). ROW_NUMBER() would invent an order between users the data says are level. The window can sit in the same SELECT as the GROUP BY because windows are evaluated after grouping.

Step 3: Filter on the position one level up

A window result does not exist yet when WHERE runs, so rnk <= 5 lives in an outer query over the derived table ranked. That filter keeps five positions: if two users ever tie for fifth, both stay on the board.

Aggregate, then dense rank, then filter on the position
SELECT user_id, total_spend, rnk
FROM (
    SELECT
        user_id,
        SUM(total_amount) AS total_spend,
        DENSE_RANK() OVER (ORDER BY SUM(total_amount) DESC) AS rnk
    FROM transactions
    GROUP BY user_id
) ranked
WHERE rnk <= 5
ORDER BY rnk, user_id;
Five rows
ORDER BY total_spend DESC LIMIT 5 stops after the fifth row it emits. With a tie at fifth, the engine's sort decides which of the level users survives, and the other one vanishes.
Five positions
DENSE_RANK() plus WHERE rnk <= 5 keeps every user whose total sits in the first five positions, so a tie grows the board instead of truncating it.

Common follow-up questions