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;
namescorerow_numrankdense_rank
Alice95111
Bob90222
Carol90322
Dave85443
  • 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 more ORDER BY columns to fully disambiguate). No two rows ever share a ROW_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 — 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.