Phase 8 · SQL interview problems, from easy to hard · Topic 8.13

Which products make up 80% of revenue? (share of total)

Should knowInterview problemMedium

Show each product's revenue, its percentage of total revenue, and the running (cumulative) percentage from the best seller down. Then list the products that bring in the first 80% of revenue.

Not started

Step 1 Solve it yourself first

Tables you will use

order_items products

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]
Sorted from best seller down, the running share climbs fast and then flattens. The first three products already pass 80% — that is the Pareto pattern.

The approach, step by step

  1. Total revenue per product in a : SUM(quantity × price).
  2. Share of total: revenue ÷ SUM(revenue) OVER (), times 100.0.
  3. Cumulative share: a running ordered by revenue descending, with ROWS UNBOUNDED PRECEDING, divided by the total.
  4. For "the first 80%": keep products whose revenue before them is still under 80% of the total.

Your turn: write it before you look

Ctrl/⌘ + Enter runs it. Select part of the query to run only that part.
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

sql
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_namerevenuepct_of_totalcumulative_pct
Headphones44055.555.5
Desk lamp15018.974.4
Backpack12015.189.5
Mug486.195.6
Notebook253.298.7
Pen101.3100

Another way: The products that make up the first 80%

sql
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_namerevenue
Headphones440
Desk lamp150
Backpack120

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.

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.

  1. 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.

  2. 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%.

    sql
    WITH 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

    namespendpct_of_totalcumulative_pct
    Priya42553.653.6
    Tom27534.788.3
    Lena607.695.8
    Zoe334.2100
  3. 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).

    sql
    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.9 * (SELECT SUM(revenue) FROM product_revenue)
    ORDER BY revenue DESC;

    4 rows

    product_namerevenue
    Headphones440
    Desk lamp150
    Backpack120
    Mug48
  4. 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.

    sql
    WITH 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_namerevenuepct_of_totalcumulative_pct
    Headphones44055.555.5
    Desk lamp15018.974.4
    Backpack12015.189.5
    Mug486.195.6
    Notebook253.298.7
    Pen101.3100
    Phone case00100

Practice box: write and run your answers here

Ctrl/⌘ + Enter runs it. Select part of the query to run only that part.

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.