Explain the differences between ROW_NUMBER(), RANK(), and DENSE_RANK()
5 minintermediatewindow-functionsrow-numberrankdense-rank
Quick Answer
All three assign a sequential position based on ORDER BY within a window, but they handle ties differently. ROW_NUMBER() gives every row a unique, sequential number regardless of ties, arbitrarily breaking them. RANK() gives tied rows the same rank, then skips the next rank number(s) by the count of ties. DENSE_RANK() gives tied rows the same rank too, but never skips subsequent rank numbers.
Detailed Answer
Side-by-side example
SELECT
name, score,
ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
RANK() OVER (ORDER BY score DESC) AS rank,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM contestants;
| name | score | row_num | rank | dense_rank |
|---|---|---|---|---|
| Alice | 95 | 1 | 1 | 1 |
| Bob | 90 | 2 | 2 | 2 |
| Carol | 90 | 3 | 2 | 2 |
| Dave | 85 | 4 | 4 | 3 |
ROW_NUMBER(): Bob and Carol are tied at 90, but get distinct numbers (2, 3) anyway. The tie is broken arbitrarily, or deterministically if you add moreORDER BYcolumns to fully disambiguate. No two rows ever share aROW_NUMBER().RANK(): Bob and Carol both get rank 2 (tied), and Dave — the next distinct value — gets rank 4, skipping 3 entirely, because two rows already "used up" ranks 2 and 3.DENSE_RANK(): Bob and Carol both get rank 2, and Dave gets rank 3, with no gap, because dense rank only increments for each distinct value encountered, not for each row.
When to use which
ROW_NUMBER(): when you need a strictly unique sequential identifier per row regardless of ties — e.g., picking exactly one "first" row per group (deduplication, pagination, "keep only the latest record per customer").RANK(): when ties should share a rank, and you want the rank to reflect "how many rows are strictly better," including the tie count — e.g., a leaderboard where two people tied for 2nd place means the next person really is in "4th place" by count.DENSE_RANK(): when ties should share a rank, but you want ranks to be a compact, gapless sequence — e.g., "top 3 distinct price tiers," where you care about distinct values, not row counts.
A common use: "top N per group"
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn
FROM employees
) ranked
WHERE rn <= 3; -- top 3 highest-paid employees PER department
This pattern (ROW_NUMBER() + PARTITION BY + filter in an outer query) is one of the most common real-world uses of window functions, and a frequent live-coding interview exercise.