Phase 5 · Window functions: ranking, running totals and comparing rows · Topic 5.5

Share of total, NTILE and FIRST_VALUE

Should know

x / SUM(x) OVER () gives each row's share of the total; NTILE(n) splits ordered rows into n roughly equal buckets; FIRST_VALUE and LAST_VALUE pull a value from the first or last row of the window.

Not started

Step 1 Understand it

Why you need it. "What percentage of revenue does each category bring?", "put customers into spending quartiles", "show the top earner's name next to everyone in their department" — all common analytics asks that answer in one line.

Watch it happen

AnimationShare of the total, and dealing rows into buckets
Read the animation as text
  1. Revenue per category, worked out in a CTE first.
  2. SUM(revenue) OVER () — an empty OVER — is one window of every row, so the grand total, 793, is written on every row.
  3. Divide each row by it: Electronics is 100.0 × 440 ÷ 793 = 55.5%. (100.0, not 100, avoids integer division.)

Picture it

pie title Share of revenue by category
  "Electronics" : 440
  "Home" : 150
  "Bags" : 120
  "Kitchen" : 48
  "Stationery" : 35
Each slice is one 's revenue divided by (revenue) () — the grand total of 793 that the window puts on every row.

The idea

SUM(revenue) OVER () is the grand total, written on every row, so 100.0 * revenue / SUM(revenue) OVER () is the percentage of the total. With you get the share within each group instead. NTILE(4) OVER (ORDER BY salary DESC) deals the sorted rows into 4 buckets, like dealing cards: 1 is the top quarter. FIRST_VALUE(name) OVER (PARTITION BY dept_id ORDER BY salary DESC) is the name on the first row of each department's window — the top earner.

The shape of it

sql
100.0 * x / SUM(x) OVER (PARTITION BY g)
NTILE(4) OVER (ORDER BY x DESC)
FIRST_VALUE(col) OVER (PARTITION BY g ORDER BY x DESC)

Words in lower case are the parts you fill in.

How it works, step by step

  1. A adds up revenue per category: Electronics 440, Home 150, Bags 120, Kitchen 48, Stationery 35.
  2. SUM(revenue) OVER () is 793 on every row.
  3. 100.0 × 440 ÷ 793 = 55.5% for Electronics, and so on.
  4. NTILE(4) over 8 employees sorted by salary gives buckets of 2: 1, 1, 2, 2, 3, 3, 4, 4.
  5. FIRST_VALUE(name), ordered by salary within each department, is that department's top earner.

Analogy

A pie chart, a league split into divisions, and a team captain. is each slice of the pie; puts teams into divisions by points; is the captain's name printed on every player's shirt.

Where the analogy breaks

Divisions in a league can be unequal by design. NTILE always makes buckets as equal as possible, so tied values can land in different buckets — fine for "roughly top 25%", wrong if ties must stay together.

Worked examples

Each category's share of revenue

sql
WITH by_category AS (
  SELECT p.category, SUM(oi.quantity * p.price) AS revenue
  FROM order_items oi
  JOIN products p ON p.product_id = oi.product_id
  GROUP BY p.category
)
SELECT category, revenue,
       ROUND(100.0 * revenue / SUM(revenue) OVER (), 1) AS pct_of_total
FROM by_category
ORDER BY revenue DESC;

5 rows

categoryrevenuepct_of_total
Electronics44055.5
Home15018.9
Bags12015.1
Kitchen486.1
Stationery354.4

NTILE: salary quartiles

sql
SELECT name, salary,
       NTILE(4) OVER (ORDER BY salary DESC, name) AS quartile
FROM employees
ORDER BY salary DESC, name;

8 rows

namesalaryquartile
Asha950001
Hana800001
Ben700002
Chen700002
Dara600003
Fatima520003
Eli450004
Gus380004

FIRST_VALUE: the top earner on every row

sql
SELECT dept_id, name, salary,
       FIRST_VALUE(name) OVER (PARTITION BY dept_id ORDER BY salary DESC) AS top_earner
FROM employees
WHERE dept_id IS NOT NULL
ORDER BY dept_id, salary DESC, name;

7 rows

dept_idnamesalarytop_earner
1Asha95000Asha
1Hana80000Asha
1Ben70000Asha
1Chen70000Asha
2Dara60000Dara
2Eli45000Dara
3Fatima52000Fatima

Must-know points

  • SUM(x) OVER () = grand total on every row; SUM(x) OVER (PARTITION BY g) = group total.
  • Share of total: 100.0 * x / SUM(x) OVER (…) — the 100.0 avoids .
  • NTILE(n) makes n buckets as equal in size as possible, in the window's order.
  • FIRST_VALUE is safe with the default ; LAST_VALUE needs ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.
  • You can use a window function over an (SUM(SUM(x)) OVER ()) in a , but a CTE is easier to read.

Common mistakes

  • Integer division in a share: revenue / SUM(revenue) OVER () gives 0 for every row.
  • Using LAST_VALUE with the default frame and getting the current row's value.
  • Expecting NTILE to keep tied values in the same bucket.

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 why SUM(revenue) OVER () puts the same number on every row.

    Show answer

    OVER () is an empty window. There is no PARTITION BY, so every row is in one big window: the whole result. There is no , so there is no running frame: the window is always all of it. SUM of the same rows is the same number, so every row gets the grand total.

    That is exactly what share of total needs: each row divided by the same grand total.

  2. Writefrom a blank page, then run it

    For each customer, show their total spend and their percentage of all spending, rounded to 1 decimal place.

    Show answer

    First add up each customer (the CTE), then divide each total by the sum of all totals. Multiply by 100.0, not 100, so the division keeps its decimals.

    The rounded shares add up to 100.1, not 100. That is normal when you round each part.

    sql
    WITH per_customer AS (
      SELECT customer_id, SUM(amount) AS total
      FROM orders
      GROUP BY customer_id
    )
    SELECT c.name, p.total,
           ROUND(100.0 * p.total / SUM(p.total) OVER (), 1) AS pct_of_all
    FROM per_customer p
    JOIN customers c ON c.customer_id = p.customer_id
    ORDER BY p.total DESC;

    4 rows

    nametotalpct_of_all
    Priya42553.6
    Tom27534.7
    Lena607.6
    Zoe334.2
  3. Predictsay it before you run it

    What quartile would each employee get with NTILE(3) instead of NTILE(4)?

    Show answer

    8 rows into 3 buckets don't divide evenly, so the first buckets get the extra rows: sizes 3, 3 and 2.

    • Bucket 1: Asha, Hana, Ben.
    • Bucket 2: Chen, Dara, Fatima.
    • Bucket 3: Eli, Gus.

    Notice Ben and Chen earn the same but land in different buckets. NTILE cuts by position, not by value; the name in the ORDER BY decides which side of the cut each one lands on.

    sql
    SELECT name, salary, NTILE(3) OVER (ORDER BY salary DESC, name) AS bucket
    FROM employees
    ORDER BY salary DESC, name;

    8 rows

    namesalarybucket
    Asha950001
    Hana800001
    Ben700001
    Chen700002
    Dara600002
    Fatima520002
    Eli450003
    Gus380003
  4. Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.

    Write LAST_VALUE(name) for the lowest earner in each department and make it return the right name on every row.

    Show answer

    Sort each department from highest to lowest salary, so the lowest earner is the last row. Then set the frame to the whole partition: ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.

    Without that frame, the default ends at the current row, so on each row "the last row of the window" is the current row: every person is named as their own lowest earner.

    In department 1, Ben and Chen tie for lowest. The name in the ORDER BY makes Chen the last row, so the tie is broken the same way every time.

    sql
    SELECT dept_id, name, salary,
           LAST_VALUE(name) OVER (
             PARTITION BY dept_id
             ORDER BY salary DESC, name
             ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
           ) AS lowest_earner
    FROM employees
    WHERE dept_id IS NOT NULL
    ORDER BY dept_id, salary DESC, name;

    7 rows

    dept_idnamesalarylowest_earner
    1Asha95000Chen
    1Hana80000Chen
    1Ben70000Chen
    1Chen70000Chen
    2Dara60000Eli
    2Eli45000Eli
    3Fatima52000Fatima

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.

For share-of-total I divide each row by SUM over an empty or partitioned window, multiplying by 100.0 to avoid integer division. NTILE buckets ordered rows into n near-equal groups for quartiles or deciles, and FIRST_VALUE picks the first row's value in each window — for LAST_VALUE I widen the frame to unbounded following, because the default frame stops at the current row.

Follow-up questions

How would you show each product's share of revenue within its category?

Use SUM(revenue) OVER (PARTITION BY category) as the divisor.

NTILE(4) on 10 rows — how big are the buckets?

3, 3, 2, 2. The leftover rows go to the first buckets.

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.