Skip to content
SQLSimplified

RANK vs DENSE_RANK vs ROW_NUMBER in SQL

RANK and DENSE_RANK can let more rows through a top-N filter than you asked for. See the tie that breaks WHERE rnk <= 3 and how to fix it.

Window FunctionsInterview QuestionsIntermediate SQL

The top-3 query that returns four rows

You ask for the top 3 earners in a department. You rank by salary, filter on rnk <= 3, and get four rows back.

Loading playground environment...

Nothing is broken. Carla and the new hire, Nora, both earn $105,000, so both land on rank 3, and rnk <= 3 matches both of them. This is the part every RANK-vs-ROW_NUMBER comparison shows you the numbering for but skips the consequence of: RANK and DENSE_RANK can both let more rows through a "top N" filter than N, because a tie at the cutoff doesn't get resolved, it gets duplicated. Only ROW_NUMBER guarantees exactly N rows, and it buys that guarantee by picking an arbitrary winner between Carla and Nora, which is its own problem, covered further down.

The three numbering rules, side by side

Run all three functions over the same tied data and the difference in rules is easy to see in one table.

Loading playground environment...

Six rows, one tie at $105,000. rnk reads 1, 2, 3, 3, 5, 6: the tied pair both get 3, and the next rank jumps to 5, because rank counts position, and two rows are occupying position 3 and 4 at once. dense_rnk reads 1, 2, 3, 3, 4, 5: the tie is still shared, but the next value is just "one more than the last distinct rank," so nothing gets skipped. rn reads 1 through 6 with no repeats at all, because ROW_NUMBER doesn't rank, it just counts, and a count can't tie.

Where RANK and DENSE_RANK actually differ

They agree right up until the row after a tie. Look at Brian in the result above: rnk gives him 5 because two rows already used up ranks 3 and 4. dense_rnk gives him 4, because it only counts distinct salary values seen so far, not rows. If downstream code does arithmetic on the rank, like "this many tiers above the median," that gap changes the answer.

ROW_NUMBER doesn't fix the tie, it hides it

Swap RANK for ROW_NUMBER in the original top-3 query and you do get exactly three rows back:

Loading playground environment...

Exactly three, as promised. But look at which one of Carla and Nora made the cut. ORDER BY salary DESC only constrains the order by salary; it says nothing about what happens when two rows have the same salary, so the SQL standard leaves the tie-break unspecified. The engine is free to hand rank 3 to whichever of the tied rows it happens to scan first, and that choice isn't guaranteed to be the same every time the query plan changes, even though the ORDER BY looks complete to a human reading it.

An unspecified tie-break is still a decision

WHERE rn <= 3 with a tie doesn't ask "who deserves the third spot," it asks "whoever the engine lists first among the tied rows." If two applicants, two transactions, or two employees are tied and only one can win a limited slot, letting ROW_NUMBER pick silently is choosing a tie-break rule without writing one down.

Making the tie-break explicit

The fix isn't a different window function, it's a fuller ORDER BY. Add a second column that always differs between tied rows, and the pick becomes reproducible instead of accidental:

Loading playground environment...

, first_name ASC breaks the $105,000 tie alphabetically, so Carla wins rank 3 every time this runs, on any engine, regardless of scan order. Whether alphabetical is the right tie-break depends on the report (hire date for "whoever's been here longest," employee ID for "whoever was hired first"), but some explicit column has to be there. The same fix applies to RANK and DENSE_RANK too, it won't stop them from returning extra rows at a genuine tie, but it makes which rows tie, and in what order they print, deterministic rather than a side effect of how the engine happened to read the table.

Picking the right one

  • ROW_NUMBER: you need exactly N rows, no more, no less, deduplicating or paginating. Add a tie-break column or accept that ties resolve arbitrarily.
  • RANK: you're showing people their position, like a leaderboard, and two people tied for 2nd should both see "2nd," with 3rd left empty rather than reused.
  • DENSE_RANK: you care about how many distinct tiers exist, like "how many different salary levels are there in this department," where a gap in the numbering would overstate how many tiers there really are.

Where to go next

The RANK() per-group example, DENSE_RANK() example, and ROW_NUMBER() example each walk the single function in more depth with its own variations. For a worked case where DENSE_RANK is the right tool because a plain MAX subquery and a LIMIT/OFFSET approach both handle ties differently, see how do you find the second highest salary in SQL. The practice problems are a good place to try writing a top-N query with a deliberate tie in the data and confirming your ORDER BY picks the row you meant it to.

Cite this resource

SQLSimplified. "RANK vs DENSE_RANK vs ROW_NUMBER in SQL". Available at: https://sqlsimplified.online/blog/rank-vs-dense-rank-vs-row-number-in-sql