NTILE, CUME_DIST & PERCENT_RANK
Split rows into equal buckets with NTILE and measure a row's relative position (0 to 1) with CUME_DIST and PERCENT_RANK.
Sometimes you do not need "1st, 2nd, 3rd" but "top third vs bottom third" or "better than 80% of innings". NTILE cuts the sorted rows into buckets; CUME_DIST and PERCENT_RANK express a row's position as a fraction between 0 and 1. These examples use the 11 innings with a score (WHERE runs IS NOT NULL).
Definition
NTILE(n) divides the ordered rows of each partition into n buckets (groups) of as equal size as possible and returns the bucket number (1 to n) for each row. If the rows do not divide evenly, the earlier buckets get one extra row.
CUME_DIST() (cumulative distribution) returns the fraction of rows whose value is less than or equal to the current row's value: rows with value ≤ current ÷ total rows. Result is greater than 0 and up to 1.
PERCENT_RANK() returns the relative rank of a row: (RANK − 1) ÷ (total rows − 1). The first row is always 0 and the last is 1.
- Buckets are about row counts, not values — NTILE can put two tied scores into different buckets.
- CUME_DIST uses the position of the last tied row; PERCENT_RANK uses RANK, i.e. the first tied position. Ties always get the same value in both.
Syntax
sql
NTILE(number_of_buckets) OVER ([PARTITION BY ...] ORDER BY ...)
CUME_DIST() OVER ([PARTITION BY ...] ORDER BY ...)
PERCENT_RANK() OVER ([PARTITION BY ...] ORDER BY ...)ORDER BY is required; a frame is not allowed.
Example — NTILE(2), NTILE(3), NTILE(4)
sql
SELECT id, player, runs,
NTILE(2) OVER (ORDER BY runs DESC, id) AS b2,
NTILE(3) OVER (ORDER BY runs DESC, id) AS b3,
NTILE(4) OVER (ORDER BY runs DESC, id) AS b4
FROM innings
WHERE runs IS NOT NULL;| id | player | runs | b2 | b3 | b4 |
|---|---|---|---|---|---|
| 12 | Jos Buttler | 107 | 1 | 1 | 1 |
| 1 | Virat Kohli | 82 | 1 | 1 | 1 |
| 5 | Rohit Sharma | 76 | 1 | 1 | 1 |
| 3 | Virat Kohli | 73 | 1 | 1 | 2 |
| 10 | Jos Buttler | 73 | 1 | 2 | 2 |
| 2 | Virat Kohli | 59 | 1 | 2 | 2 |
| 11 | Jos Buttler | 59 | 2 | 2 | 3 |
| 4 | Rohit Sharma | 45 | 2 | 2 | 3 |
| 9 | MS Dhoni | 45 | 2 | 3 | 3 |
| 7 | MS Dhoni | 26 | 2 | 3 | 4 |
| 6 | Rohit Sharma | 0 | 2 | 3 | 4 |
How the sizes are decided: 11 rows ÷ n buckets.
- NTILE(2): 11 ÷ 2 = 5 remainder 1 → sizes 6, 5 (high vs low scorers).
- NTILE(3): 11 ÷ 3 = 3 remainder 2 → sizes 4, 4, 3 (top / middle / bottom third).
- NTILE(4): 11 ÷ 4 = 2 remainder 3 → sizes 3, 3, 3, 2 (quartiles).
Notice the two 73s land in different buckets for NTILE(3) — NTILE cuts by row count and ignores ties. Use cases: customer segmentation (High / Medium / Low), quartiles, and splitting a big table into equal batches for processing.
Example — CUME_DIST and PERCENT_RANK
sql
SELECT id, player, runs,
ROUND(CUME_DIST() OVER (ORDER BY runs), 3) AS cume_dist,
ROUND(PERCENT_RANK() OVER (ORDER BY runs), 3) AS pct_rank
FROM innings
WHERE runs IS NOT NULL;| id | player | runs | cume_dist | pct_rank |
|---|---|---|---|---|
| 6 | Rohit Sharma | 0 | 0.091 | 0 |
| 7 | MS Dhoni | 26 | 0.182 | 0.1 |
| 4 | Rohit Sharma | 45 | 0.364 | 0.2 |
| 9 | MS Dhoni | 45 | 0.364 | 0.2 |
| 2 | Virat Kohli | 59 | 0.545 | 0.4 |
| 11 | Jos Buttler | 59 | 0.545 | 0.4 |
| 3 | Virat Kohli | 73 | 0.727 | 0.6 |
| 10 | Jos Buttler | 73 | 0.727 | 0.6 |
| 5 | Rohit Sharma | 76 | 0.818 | 0.8 |
| 1 | Virat Kohli | 82 | 0.909 | 0.9 |
| 12 | Jos Buttler | 107 | 1 | 1 |
Working it out (11 rows, lowest first):
- CUME_DIST of 45: 4 rows have runs ≤ 45 (0, 26, 45, 45) → 4 ÷ 11 = 0.364. Both 45s get the value of the last tied position.
- PERCENT_RANK of 45: RANK of 45 is 3 → (3 − 1) ÷ (11 − 1) = 0.2.
- PERCENT_RANK of 59: RANK is 5 (both 45s are below it) → 4 ÷ 10 = 0.4.
- Lowest row: PERCENT_RANK = 0. Highest row: both = 1.
Reading it: Kohli's 82 has CUME_DIST 0.909 → "91% of innings scored 82 or less" — it is in the top 10%.
| CUME_DIST | PERCENT_RANK | |
|---|---|---|
| Formula | rows ≤ current ÷ total rows | (rank − 1) ÷ (rows − 1) |
| Lowest row | 1 ÷ n (never 0) | always 0 |
| Highest row | 1 | 1 |
| Ties use | last tied position | first tied position (RANK) |
Note: if Dhoni's NULL is included, it counts as the lowest value in ASC order and shifts every number (NULL would get CUME_DIST 0.083 and the 0 would become 0.167). That is why these examples filter NULLs out first.
Common mistakes
- ❌ Expecting NTILE to keep ties together → ✅ it splits by row count; use DENSE_RANK or CASE ranges for value-based groups.
- ❌ Thinking NTILE(3) gives buckets of equal value range → ✅ equal row counts.
- ❌ Mixing up the two percent functions → ✅ CUME_DIST includes the current row's ties ("≤"), PERCENT_RANK starts at 0.
- ❌ Not handling NULL → it is ranked as the lowest value.
Interview tip
Q: How would you split customers into 4 equal groups by total spend?
A: NTILE(4) OVER (ORDER BY total_spend DESC) — bucket 1 is the top 25% of customers. If the count does not divide evenly, the first buckets get one extra row.
Key points to remember
- NTILE(n) splits ordered rows into n buckets of near-equal size; earlier buckets get the extra rows (11 rows → NTILE(3) = 4, 4, 3).
- NTILE works on row counts, so tied values can land in different buckets.
- CUME_DIST = rows with value ≤ current ÷ total rows; range (0, 1], ties use the last tied position.
- PERCENT_RANK = (RANK − 1) ÷ (rows − 1); lowest row = 0, highest = 1.
- All three require ORDER BY in OVER and do not allow a frame.
- NULL counts as the lowest value — filter it out if it should not be ranked.
Try it yourself
NTILE buckets
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Segmentation: High / Medium / Low
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
CUME_DIST and PERCENT_RANK
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Practice: see the effect of NULL
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Revision Flashcards
Tap a card to flip it and see the answer.
