skip to content

Ranking Functions

ROW_NUMBER, RANK, DENSE_RANK, and NTILE assign positions within each partition, and the difference shows up exactly when values tie. Explaining rank gaps versus dense ranks on tied rows is one of the most common SQL interview probes.

part ofSQLoverview, primer and where to startread it →
on this pageshow

questions

5

How do ROW_NUMBER(), RANK(), and DENSE_RANK() differ when the ORDER BY values tie?

level: juniorimportance: must knowfreq 88%

answer

  1. they only disagree on tied rows
  2. one of them never repeats a number
  3. two share a number, one leaves a hole
  4. gaps versus consecutive tier labels

basics

~20 s

ROW_NUMBER() numbers every row 1, 2, 3 with no repeats. RANK() gives tied rows the same number and then skips ahead, leaving a gap. DENSE_RANK() also gives ties the same number but continues with the next integer, no gap.

solid answer

~40 s

All three assign a position inside each window partition, ordered by the window's `ORDER BY`, and they only diverge on ties. `ROW_NUMBER()` is a plain counter: 1..N, always distinct, so tied rows get different numbers and which one goes first is arbitrary unless the ordering is unique. `RANK()` is competition ranking: it returns one plus the number of rows that strictly precede the tied group, so scores 100, 90, 90, 80 rank 1, 2, 2, 4 — the 3 is skipped. `DENSE_RANK()` counts distinct ordering values instead, giving 1, 2, 2, 3 with no gap. Pick `ROW_NUMBER()` when you need exactly one row per position, `RANK()` when "joint second, next is fourth" is the business rule, and `DENSE_RANK()` when you are labelling tiers or counting distinct levels.

code

sql · 9 lines
sql
SELECT player, points,
       ROW_NUMBER() OVER (ORDER BY points DESC) AS rn,
       RANK()       OVER (ORDER BY points DESC) AS rnk,
       DENSE_RANK() OVER (ORDER BY points DESC) AS dns
FROM scores;
-- ana 100 -> rn 1, rnk 1, dns 1
-- bo   90 -> rn 2, rnk 2, dns 2
-- cy   90 -> rn 3, rnk 2, dns 2
-- di   80 -> rn 4, rnk 4, dns 3

go deeper

for a junior

Memorise one worked example, such as 100, 90, 90, 80 producing 1,2,3,4 for ROW_NUMBER, 1,2,2,4 for RANK and 1,2,2,3 for DENSE_RANK. Being able to write those three rows out is most of the answer.

for a middle

Explain the definitions, not just the example: RANK is one plus the rows preceding the peer group, DENSE_RANK counts distinct ordering values. Be ready to say what each returns after a three-way tie.

for a senior

Show that you pick the function from the business rule, and that you know ROW_NUMBER needs a unique tiebreaker before anything downstream depends on which row got 1.

for a principal

Own the reporting consequence: swapping RANK for DENSE_RANK silently redefines what a published standing or tier means, so the choice belongs in the metric definition and should be reviewed like any other business rule.

## What a ranking function computes A ranking function assigns a position to each row inside a *window* — the row set described by `OVER (PARTITION BY ... ORDER BY ...)`. `PARTITION BY` splits rows into independent groups and the numbering restarts at 1 in every group; the window `ORDER BY` decides the sequence in which positions are handed out. Nothing is collapsed: every input row survives and simply gains one extra column. Two rows are **peers** when the window `ORDER BY` cannot tell them apart — all the ordering expressions compare equal. Ties are exactly where the three functions part company. ## The three definitions - `ROW_NUMBER()` — a plain counter over the ordered partition: 1, 2, 3, … N. Values are always distinct, so peers are forced into some order and given different numbers. - `RANK()` — one plus the number of rows that strictly precede the current row's peer group. All peers share a number, and the next distinct value resumes at its own ordinal position, which produces gaps. - `DENSE_RANK()` — the count of distinct ordering values at or before the current row. All peers share a number, and the next distinct value is simply the next integer, so there are no gaps. ## Worked example ```sql SELECT player, points, ROW_NUMBER() OVER (ORDER BY points DESC) AS rn, RANK() OVER (ORDER BY points DESC) AS rnk, DENSE_RANK() OVER (ORDER BY points DESC) AS dns FROM scores; ``` With rows `ana 100`, `bo 90`, `cy 90`, `di 80` the output is: - ana: rn 1, rnk 1, dns 1 - bo: rn 2, rnk 2, dns 2 - cy: rn 3, rnk 2, dns 2 - di: rn 4, rnk 4, dns 3 Note two things. First, `bo` and `cy` are peers, so `RANK()` and `DENSE_RANK()` treat them identically while `ROW_NUMBER()` had to pick an order — whether `bo` gets 2 and `cy` gets 3 or the other way round is not determined by the query. Second, `di` shows the gap: `RANK()` says 4 because three rows precede it, `DENSE_RANK()` says 3 because it is the third distinct score. The gap widens with the size of the tie. If three players all scored 100, `RANK()` gives them 1, 1, 1 and the next distinct score gets 4; `DENSE_RANK()` gives 1, 1, 1 and then 2. ## Why gaps are a feature, not a bug The two behaviours answer different questions. `RANK()` answers "how many competitors are ahead of me, plus one?" — the classic medal-table rule where two silver medals mean nobody gets bronze. `DENSE_RANK()` answers "how many distinct levels exist at or above mine?" — the right shape when you are labelling salary bands, price tiers or grade levels, because you want consecutive labels regardless of how many people sit on each level. Neither is more correct; picking the wrong one silently changes what a report means. ## Choosing between them - Need exactly one row per position — one row to keep, one row per page slot, a strict 1..N sequence? `ROW_NUMBER()`. - Need standings where ties genuinely share a place and the following place absorbs the gap? `RANK()`. - Need consecutive tier labels, or the number of distinct values encountered so far? `DENSE_RANK()`. ## Determinism footnote `RANK()` and `DENSE_RANK()` are stable under ties by construction: peers always receive the same number, so re-running the query cannot shuffle their values. `ROW_NUMBER()` has to break every tie somehow, and the standard does not say how — the assignment among peers is implementation-defined and can change between runs, plans, or engine versions. If which specific row receives 1 matters, extend the window `ORDER BY` with a column combination that is unique, such as the primary key. ## Common traps Candidates often say the three are interchangeable, or that `ROW_NUMBER()` repeats numbers for equal values. Others treat the missing 3 in a `RANK()` result as an engine defect. A third trap is forgetting that with `PARTITION BY` every group starts again at 1, so the same number appears many times across the result — that is partitioning, not tie behaviour.

  • Can two rows in the same partition ever receive the same ROW_NUMBER() value?
    No. Within one partition `ROW_NUMBER()` produces each of 1..N exactly once, so peers are forced apart. The same number does reappear in other partitions, because `PARTITION BY` restarts the counter at 1 for every group — that is partitioning, not a tie.
  • Three players share the top score. What does RANK() give the next distinct score, and what does DENSE_RANK() give?
    `RANK()` gives 4: it returns one plus the number of rows strictly preceding the peer group, and three rows precede. `DENSE_RANK()` gives 2, because it counts distinct ordering values and this is the second distinct score. The bigger the tie, the wider the gap `RANK()` leaves.
  • Do these functions depend on the window frame?
    No. `ROW_NUMBER()`, `RANK()` and `DENSE_RANK()` are defined over the whole ordered partition, so their result is fixed by `PARTITION BY` and the window `ORDER BY` alone. That is unlike windowed aggregates such as `SUM(...) OVER (...)`, whose value is computed over a frame of rows.

RANK() is Olympic scoring: two joint silvers and nobody gets bronze. DENSE_RANK() is medal types: gold, silver, bronze exist regardless of how many people share each. ROW_NUMBER() is the finishing-line queue, where somebody must be handed the next ticket even in a photo finish.

saying these in an interview costs you the question

  • Says RANK() and DENSE_RANK() are interchangeable
  • Thinks ROW_NUMBER() gives tied rows the same number
  • Calls RANK()'s skipped numbers an engine bug
  • Forgets PARTITION BY restarts the numbering at 1
  • Claims ROW_NUMBER() output is stable without a unique tiebreaker

context

open as a page

Why can ROW_NUMBER() give the same rows different numbers on repeated runs?

level: middleimportance: must knowfreq 55%

basics

~20 s

ROW_NUMBER() numbers rows in the window ORDER BY sequence, but when that ordering is not unique the order among tied rows is left undefined, so re-runs may number them differently. Add a unique tiebreaker column to make the ordering total.

open as a page

How does NTILE(4) distribute rows when the row count is not divisible by 4?

level: middleimportance: should knowfreq 40%

basics

~20 s

NTILE(n) splits the ordered partition into n buckets as evenly as possible: with R rows, the first R mod n buckets get one extra row each. Ten rows into four buckets gives sizes 3, 3, 2, 2.

open as a page

NTILE(10) puts identical spend values in different deciles and boundaries move as data grows - how do you get stable segments?

level: seniorimportance: should knowfreq 32%

basics

~20 s

NTILE(10) guarantees equal-sized buckets, not equal value ranges, so identical amounts can fall either side of a boundary and cut-points drift as the population changes. For segments that depend on the value itself, bucket on explicit cut-points with a CASE expression.

open as a page

What is the difference between PERCENT_RANK() and CUME_DIST() for tied rows?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

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

open as a page