Step 1 Solve it yourself first
Tables you will use
What it really tests
Window over the whole result (), a with an explicit frame, and turning a cumulative share into a filter — the classic analysis.
The pattern: Share of total and cumulative share
"Percentage of total", "share", "contribution", "cumulative", "top products making up X%".
Picture it
xychart-beta title "Cumulative share of revenue" x-axis ["Headphones", "Desk lamp", "Backpack", "Mug", "Notebook", "Pen"] y-axis "Cumulative %" 0 --> 100 bar [55.5, 74.4, 89.5, 95.6, 98.7, 100]
The approach, step by step
- Total revenue per product in a :
SUM(quantity × price). - Share of total: revenue ÷
SUM(revenue) OVER (), times 100.0. - Cumulative share: a running ordered by revenue descending, with ROWS UNBOUNDED PRECEDING, divided by the total.
- For "the first 80%": keep products whose revenue before them is still under 80% of the total.
Your turn: write it before you look
Your queries in this box
Show the solution
Stuck for more than ten minutes? Open it, read it, close it, and write it again from memory.
Solution: share and running share with
WITH product_revenue AS (
SELECT p.product_name, SUM(oi.quantity * p.price) AS revenue
FROM order_items oi
JOIN products p ON p.product_id = oi.product_id
GROUP BY p.product_name
)
SELECT product_name, revenue,
ROUND(100.0 * revenue / SUM(revenue) OVER (), 1) AS pct_of_total,
ROUND(100.0 * SUM(revenue) OVER (ORDER BY revenue DESC ROWS UNBOUNDED PRECEDING)
/ SUM(revenue) OVER (), 1) AS cumulative_pct
FROM product_revenue
ORDER BY revenue DESC;6 rows
| product_name | revenue | pct_of_total | cumulative_pct |
|---|---|---|---|
| Headphones | 440 | 55.5 | 55.5 |
| Desk lamp | 150 | 18.9 | 74.4 |
| Backpack | 120 | 15.1 | 89.5 |
| Mug | 48 | 6.1 | 95.6 |
| Notebook | 25 | 3.2 | 98.7 |
| Pen | 10 | 1.3 | 100 |
Another way: The products that make up the first 80%
WITH product_revenue AS (
SELECT p.product_name, SUM(oi.quantity * p.price) AS revenue
FROM order_items oi
JOIN products p ON p.product_id = oi.product_id
GROUP BY p.product_name
),
running AS (
SELECT product_name, revenue,
SUM(revenue) OVER (ORDER BY revenue DESC ROWS UNBOUNDED PRECEDING) - revenue AS revenue_before
FROM product_revenue
)
SELECT product_name, revenue
FROM running
WHERE revenue_before < 0.8 * (SELECT SUM(revenue) FROM product_revenue)
ORDER BY revenue DESC;3 rows
| product_name | revenue |
|---|---|
| Headphones | 440 |
| Desk lamp | 150 |
| Backpack | 120 |
A product is in the first 80% if the revenue before it hadn't reached 80% yet. Backpack starts at 74.4%, so it is the one that crosses the line.
Edge cases to say out loud
- Ties in revenue: the running total needs a (e.g. product name), or tied products share a RANGE frame.
- Products that never sold (the phone case) don't appear; from products if they should show 0%.
- Rounding: the rounded percentages may not add up to exactly 100.
Common mistakes
- :
revenue / SUM(revenue) OVER ()is 0 without the 100.0. - Leaving the frame as the default RANGE, so tied revenues get the same cumulative value.
- Filtering
cumulative_pct <= 80, which drops the product that crosses 80% — usually it should be included.
Step 2 Go further (free, optional)
Optional. The notes above are written to be enough on their own. These free resources give a second explanation and more practice.
-
Read
Mode SQL Tutorial — SQL Window Functions
Mode, free
-
Practice
DataLemur — percentage of total questions
real interview questions, many free
Step 3 Practise
Try each one yourself first: out loud, or written and run in the practice box below, which saves every query you run. Then open the answer and compare.
-
ExplainSay what it does and when to use it, in plain words, with no notes.
Explain the difference between the pct_of_total and cumulative_pct .
Show answer
pct_of_total is each product on its own: its revenue ÷ total revenue. The headphones bring in 440 of 793, so 55.5%. These add up to 100% across all .
cumulative_pct is a running total: this product plus every product above it in the list, ÷ total revenue. It only grows as you go down: 55.5% after the headphones, 74.4% once the desk lamp is added, 89.5% after the backpack, and 100% at the last row. It answers "how much of revenue do the top N products bring in?", which is what the 80% question needs.
-
Writefrom a blank page, then run it
Do the same analysis for customers: each customer's share of total spend and the cumulative share.
Show answer
Same structure, with customers instead of products: total each customer's spend, then divide by the grand total (empty ) and by the running total (ordered OVER). Priya alone is 53.6% of all spending, and Priya and Tom together are 88.3%.
sqlWITH customer_spend AS ( SELECT c.name, SUM(o.amount) AS spend FROM orders o JOIN customers c ON c.customer_id = o.customer_id GROUP BY c.customer_id, c.name ) SELECT name, spend, ROUND(100.0 * spend / SUM(spend) OVER (), 1) AS pct_of_total, ROUND(100.0 * SUM(spend) OVER (ORDER BY spend DESC ROWS UNBOUNDED PRECEDING) / SUM(spend) OVER (), 1) AS cumulative_pct FROM customer_spend ORDER BY spend DESC;4 rows
name spend pct_of_total cumulative_pct Priya 425 53.6 53.6 Tom 275 34.7 88.3 Lena 60 7.6 95.8 Zoe 33 4.2 100 -
Predictsay it before you run it
Which products would be in "the first 90% of revenue"?
Show answer
Headphones, desk lamp, backpack and mug.
The first three only reach 89.5%, just short of 90%, so you need the mug too (it takes the total to 95.6%). The rule in the alternative is "keep a product if the revenue before it was still under the target"; with 0.9 instead of 0.8, the mug passes (89.5% before it) and the notebook doesn't (95.6% before it).
sqlWITH product_revenue AS ( SELECT p.product_name, SUM(oi.quantity * p.price) AS revenue FROM order_items oi JOIN products p ON p.product_id = oi.product_id GROUP BY p.product_name ), running AS ( SELECT product_name, revenue, SUM(revenue) OVER (ORDER BY revenue DESC ROWS UNBOUNDED PRECEDING) - revenue AS revenue_before FROM product_revenue ) SELECT product_name, revenue FROM running WHERE revenue_before < 0.9 * (SELECT SUM(revenue) FROM product_revenue) ORDER BY revenue DESC;4 rows
product_name revenue Headphones 440 Desk lamp 150 Backpack 120 Mug 48 -
Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.
Make the phone case appear with 0 revenue and 0%, without breaking the cumulative column.
Show answer
Start from products with a LEFT JOIN, so the phone case has a row, and its revenue to 0. A zero adds nothing to any sum, so the totals and the running column are unchanged: the phone case sorts last, with 0% and a cumulative 100%.
Add product_name to both ORDER BYs (the window's and the query's). That way two products with equal revenue, such as several unsold ones at 0, always come out in the same order, and the running total doesn't depend on chance.
sqlWITH product_revenue AS ( SELECT p.product_name, COALESCE(SUM(oi.quantity * p.price), 0) AS revenue FROM products p LEFT JOIN order_items oi ON oi.product_id = p.product_id GROUP BY p.product_name ) SELECT product_name, revenue, ROUND(100.0 * revenue / SUM(revenue) OVER (), 1) AS pct_of_total, ROUND(100.0 * SUM(revenue) OVER (ORDER BY revenue DESC, product_name ROWS UNBOUNDED PRECEDING) / SUM(revenue) OVER (), 1) AS cumulative_pct FROM product_revenue ORDER BY revenue DESC, product_name;7 rows
product_name revenue pct_of_total cumulative_pct Headphones 440 55.5 55.5 Desk lamp 150 18.9 74.4 Backpack 120 15.1 89.5 Mug 48 6.1 95.6 Notebook 25 3.2 98.7 Pen 10 1.3 100 Phone case 0 0 100
Practice box: write and run your answers here
Your queries in this box
Step 4 Interview-ready, then prove it
Your interview answer
About 20 to 40 seconds, said out loud.
I total revenue per product in a CTE, then divide each by SUM over an empty window for the share of total, and use a running SUM ordered by revenue descending with an explicit ROWS frame for the cumulative share. To list the products making up the first 80%, I keep those whose revenue before them is still under 80% of the total, which includes the product that crosses the line.
Follow-up questions
How would you show each product's share within its own category?
Divide by SUM(revenue) OVER (PARTITION BY category) instead of OVER ().
Why ROWS UNBOUNDED PRECEDING rather than the default?
The default RANGE frame lumps tied values together; ROWS adds one product at a time.
Tick each check when you can do it without notes
A topic is finished when you pass all four checks, not when you have read it. Be strict: if you had to look at the notes, it is not ticked yet.
- Explain Say what it does and when to use it, in plain words, with no notes.
- Write Write the query from a blank page, then run it to check.
- Predict Read a query and say which rows it returns before you run it.
- Edge cases Say what happens with NULLs, ties, duplicates or no rows at all, and fix it.
0 of 4 checks ticked.
Saved in this browser. Back it up or move it to another device.