Consistent High-Quantity Revenue

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

Problem

A single sale of fewer than two units disqualifies a customer's whole history with a product, so only pairs where every row in the transactions table moved two or more units count. For the merchandising team's review, total each qualifying customer and product pair's revenue, reading down customer by customer from the lowest id with each customer's biggest earners first.

Big orders, consistent revenue. A rare combination.

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 80 rows
user_idproduct_idtotal_revenue
19754112471.52
19746762067.42
19739411663.32
19732061259.22
1972471855.12

Worked solution and explanation

This is a universal quantifier dressed up as a merchandising report: keep a customer and product pair only if every one of its sales moved two or more units. Anyone can total total_amount per pair. What separates candidates is where the quantity test lives. Put quantity >= 2 in WHERE and the engine throws away the short sales before the pair is ever formed, so a pair with one single-unit sale survives on its remaining rows and reports a total that is not even its real total. The rule is about the whole pair, so the test has to run after the rows are gathered.

Getting there

Step 1: Form the pair before judging it

GROUP BY user_id, product_id collects every sale of a pair, including the ones that will disqualify it. Nothing is filtered yet: a pair has to keep its bad rows to know it is bad.

Step 2: Turn 'every sale' into one number

'Every quantity is at least 2' says exactly what 'the smallest quantity is at least 2' says. MIN(quantity) collapses the pair into that one number, and HAVING MIN(quantity) >= 2 drops the pair when any sale falls short, the voided zero-unit row included.

Step 3: Sum every row of a survivor

SUM(total_amount) runs over the same group, so a surviving pair reports all of its revenue, refunds included. total_amount already holds each sale's revenue; nothing is recomputed from quantity.

Step 4: Lay it out customer by customer

ORDER BY user_id ASC, total_revenue DESC sorts on the alias the SELECT computed, so the reader scans one customer at a time with that customer's biggest pair on top.

Every sale clears the bar, or the pair is gone
SELECT
    user_id,
    product_id,
    SUM(total_amount) AS total_revenue
FROM transactions
GROUP BY user_id, product_id
HAVING MIN(quantity) >= 2
ORDER BY user_id ASC, total_revenue DESC
Row filter
WHERE quantity >= 2 runs before grouping. A pair with sales of 3, 1 and 4 units loses the 1-unit row and comes back with the revenue of the other two: a pair the rule excludes, reported with a shrunken total.
Group gate
HAVING MIN(quantity) >= 2 runs after grouping. The same pair has a minimum of 1 and disappears entirely; every pair that survives is summed over all of its rows.

Common follow-up questions