What is the difference between PERCENT_RANK() and CUME_DIST() for tied rows?
answer
- both are peer-aware, ties share a value
- one is a rescaled RANK()
- one always starts at 0, one always ends at 1
- one measures the tie group's start, the other its end
basics
~20 sBoth give every tied row the same value. PERCENT_RANK() is (RANK() - 1) / (rows - 1), so the first row is always 0. CUME_DIST() is the fraction of rows at or before the tie group, so the last row is always 1.
solid answer
~40 sThey are the two standard relative-position functions and they differ in which end of a peer group they measure from. `PERCENT_RANK()` is defined as `(RANK() - 1) / (partition rows - 1)`: it uses the **first** position of the peer group, so the lowest row scores 0 and the value climbs to 1 at the top. `CUME_DIST()` is the number of rows at or before the current row's peer group divided by the partition row count: it uses the **last** position, so it is always greater than 0 and reaches exactly 1 for the final peer group. For values 10, 20, 20, 40 ascending, `PERCENT_RANK()` gives 0, 0.333, 0.333, 1 while `CUME_DIST()` gives 0.25, 0.75, 0.75, 1. In both cases peers share a value, which is what makes them usable as percentile scores.
code
sql · 9 linesSELECT student, score,
RANK() OVER (ORDER BY score) AS rnk,
PERCENT_RANK() OVER (ORDER BY score) AS pr,
CUME_DIST() OVER (ORDER BY score) AS cd
FROM exam_results;
-- scores 10, 20, 20, 40 ->
-- rnk 1, 2, 2, 4
-- pr 0, 0.3333, 0.3333, 1
-- cd 0.25, 0.75, 0.75, 1go deeper
Recognise both as window functions that return a fraction rather than an ordinal position, and remember that rows with equal ordering values receive the same fraction.
Quote the formulas: PERCENT_RANK is (RANK - 1) / (rows - 1) starting at 0, CUME_DIST is rows at or before the peer group over total rows, ending at 1.
Pick the right one for the sentence a report will publish, and be able to explain on a tied data set why the two disagree by exactly the width of the tie.
Decide whether a percentile metric should be a continuous position score or an equal-count cohort, and record the choice, because the two answer different business questions and are not interchangeable across dashboards.
## Two ways to say "where in the distribution am I?" `PERCENT_RANK()` and `CUME_DIST()` both express a row's position in its window partition as a fraction rather than an ordinal. Both take no arguments, both need a window `ORDER BY` to mean anything, and both are peer-aware: rows whose ordering values compare equal always receive the same result. That last property is what separates them from `ROW_NUMBER()`, which would give near-identical rows visibly different scores. ## PERCENT_RANK() The definition is arithmetic on `RANK()`: PERCENT_RANK() = (RANK() - 1) / (number of rows in the partition - 1) Because `RANK()` is 1 for the first peer group, `PERCENT_RANK()` is 0 for every row in it — always, no matter how many rows tie there. The maximum is 1, reached by the last peer group. The measure therefore answers "what fraction of the other rows am I strictly ahead of?" and its range is 0 to 1 inclusive. A single-row partition would divide by zero, so the standard defines the result as 0 in that case. ## CUME_DIST() The definition counts rows instead: CUME_DIST() = (rows preceding or peer with the current row) / (number of rows in the partition) The numerator runs through the **end** of the peer group, so `CUME_DIST()` answers "what fraction of rows are at or below me?" — the cumulative distribution in the statistical sense. It is never 0, because the current row always counts itself, and it is exactly 1 for the final peer group. ## Worked comparison Take four rows with values 10, 20, 20, 40 ordered ascending. `RANK()` gives 1, 2, 2, 4. - `PERCENT_RANK()`: (1−1)/3 = 0, (2−1)/3 ≈ 0.333, ≈ 0.333, (4−1)/3 = 1. - `CUME_DIST()`: 1/4 = 0.25, 3/4 = 0.75, 0.75, 4/4 = 1. Notice both give the tied 20s one shared value, but they disagree about what that value is: `PERCENT_RANK()` reports the tie group's starting position, `CUME_DIST()` its ending position. The gap between them is exactly the width of the tie. ```sql SELECT student, score, PERCENT_RANK() OVER (ORDER BY score) AS pr, CUME_DIST() OVER (ORDER BY score) AS cd FROM exam_results; ``` ## Choosing between them Use `CUME_DIST()` when the sentence you want to write is "this row is in the top 10 percent" or "90 percent of the population scores at or below this" — it is the cumulative-distribution reading people expect from a percentile. Use `PERCENT_RANK()` when you want a normalised rank that starts at 0 for the best (or worst, depending on sort direction) row, for example to rescale positions onto a 0-to-1 axis for comparison across partitions of very different sizes. Both are relative to the partition, so with `PARTITION BY` each group is scored against itself. That makes them a natural way to compare a row against its own cohort rather than the whole table. ## Relationship to the other ranking functions All of these are computed over the whole ordered partition — `PARTITION BY` and the window `ORDER BY` fully determine the answer, and there is no frame to specify. `PERCENT_RANK()` is literally a rescaled `RANK()`, which is why it inherits `RANK()`'s gap behaviour: a wide tie near the bottom pushes the next distinct value's fraction up sharply. `CUME_DIST()` has no gap behaviour of its own; it just counts rows. A common confusion is with `NTILE(n)`, which returns a bucket number rather than a fraction and guarantees near-equal bucket sizes rather than a faithful position. If you need a continuous percentile score, these two functions are the right tools; if you need cohorts of equal size, `NTILE` is.
- What does PERCENT_RANK() return for a partition with a single row?0. The formula (RANK() - 1) / (rows - 1) would divide by zero, so the standard defines the single-row result as 0. CUME_DIST() returns 1 for the same row, because that row is at or below itself and the partition has one row in total.
- Which of the two would you use to say a customer is in the top 10 percent?CUME_DIST(), because it reports the fraction of rows at or below the current row — the cumulative-distribution reading people expect from a percentile. With a descending order, a CUME_DIST() at or under 0.1 is the top decile of the population.
- How do these differ from NTILE(10)?They return a continuous fraction that reflects a row's actual position and gives peers identical scores. NTILE(10) returns a bucket label and guarantees near-equal bucket sizes instead, which means it can split tied rows across two buckets.
saying these in an interview costs you the question
- Says PERCENT_RANK and CUME_DIST are the same function
- Thinks both range from 0 to 1 the same way
- Believes tied rows get different percentile values
- Reads CUME_DIST as a fraction of the maximum value
- Confuses either one with an NTILE bucket number