Top Revenue Products H1

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
General Assembly

Problem

Each row of transactions is one sale with its product and line total, and a sale with no product recorded still counts, pooled into one line of its own. Over January through June of every year on record, return the top 5 products by total revenue, highest first, and if products are tied at the cutoff, include all of them.

First half of the year. Which products led the revenue race?

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
product_idtotal_revenue
NULL9740.94
58522713.98
56072579.28
51662336.82
51172309.88

Worked solution and explanation

This is a top-N-with-ties question dressed up as a revenue report. The cutoff separates candidates: 'top 5' with an explicit tie clause is a standing, not a row count. ORDER BY total_revenue DESC LIMIT 5 reads right and keeps exactly five rows. So on the day two products land level at fifth, one of them vanishes, and which one is up to the engine. The window hides two more traps. January through June means those months in every year on record. The sales with no product_id are a real revenue line, and at 9740.94 it tops the list.

Step 1: Pool the first half of every year

Pull the month out of transaction_date and keep 1 through 6 without pinning a year: the table spans several years and each one contributes its January to June. strftime('%m', ...) returns text like '03', so the CAST(... AS INTEGER) is what makes BETWEEN 1 AND 6 numeric. Leave it off and text never sits between two integers, so every row is filtered out.

Step 2: Sum total_amount per product_id

total_amount is already the line total, so there is no quantity times price to compute. GROUP BY product_id collects every product-less sale into one NULL group. That is the line the statement keeps, and it is the largest. A WHERE product_id IS NOT NULL added out of habit silently promotes the sixth product into the list.

Step 3: Cut on the standing, not the row count

DENSE_RANK() OVER (ORDER BY SUM(total_amount) DESC) gives level totals the same standing, so rnk <= 5 keeps every product tied at fifth. The window is evaluated after GROUP BY, which is why it can order by the aggregate in the same SELECT. On this data nobody is level at fifth, so the list is five rows. The query stays correct the day a tie appears, and a LIMIT does not.

The solution

Top 5 products by first-half revenue
SELECT product_id, total_revenue
FROM (
  SELECT product_id,
         SUM(total_amount) AS total_revenue,
         DENSE_RANK() OVER (ORDER BY SUM(total_amount) DESC) AS rnk
  FROM transactions
  WHERE CAST(strftime('%m', transaction_date) AS INTEGER) BETWEEN 1 AND 6
  GROUP BY product_id
) ranked
WHERE rnk <= 5
ORDER BY total_revenue DESC
A five-row cap
ORDER BY total_revenue DESC LIMIT 5 stops at five rows. When two products are level at fifth, one is dropped, and which one depends on the order the engine happened to emit.
A fifth-standing cutoff
DENSE_RANK() with rnk <= 5 stops at the fifth standing. Every product level at fifth stays, so the list can grow past five and is the same on every run.

Common follow-up questions