RANK vs DENSE_RANK vs ROW_NUMBER
All three number rows by an ORDER BY, differing only on ties: ROW_NUMBER() gives every row a unique number, RANK() gives ties the same number then skips ahead (1, 2, 2, 4), and DENSE_RANK() gives ties the same number with no gaps (1, 2, 2, 3).
Quick answer: For scores 95, 90, 90, 85, ROW_NUMBER() returns 1, 2, 3, 4, RANK() returns 1, 2, 2, 4, and DENSE_RANK() returns 1, 2, 2, 3. Use ROW_NUMBER when you need exactly N rows or deduplication, RANK when tied rows should share a place and the next place is skipped, and DENSE_RANK for "Nth-highest value" questions. Add PARTITION BY inside OVER() to restart the numbering for each group.
How Do RANK, DENSE_RANK, and ROW_NUMBER Handle Ties?
Tied rows get the same number from RANK and DENSE_RANK but different numbers from ROW_NUMBER. After a tie, RANK skips ahead by the number of tied rows while DENSE_RANK continues with the next integer, so for scores 95, 90, 90, 85 the two 90s share 2nd place and the 85 is 4th by RANK but 3rd by DENSE_RANK.
SELECT name, score,
ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
RANK() OVER (ORDER BY score DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rnk
FROM results;
-- name | score | row_num | rnk | dense_rnk
-- Ana | 95 | 1 | 1 | 1
-- Ben | 90 | 2 | 2 | 2
-- Cara | 90 | 3 | 2 | 2 <- tie
-- Dev | 85 | 4 | 4 | 3 <- RANK skips, DENSE_RANK doesn't
These are window functions, so they add a column without collapsing rows โ basics in SQL window functions.
When Should You Use Each One?
Use ROW_NUMBER when you need exactly N rows or deduplication, RANK when ties should share a place Olympics-style, and DENSE_RANK when you're counting distinct value levels (e.g. "second-highest salary").
-- Second-highest distinct salary:
SELECT * FROM (
SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS r
FROM employees
) t
WHERE r = 2; -- RANK could skip 2 entirely if the top salary ties
What Is the Top-N-Per-Group Pattern?
Add PARTITION BY to restart the numbering per group, then filter on the rank in an outer query โ the standard way to get "top 3 products per category."
SELECT * FROM (
SELECT category, product, revenue,
ROW_NUMBER() OVER (
PARTITION BY category
ORDER BY revenue DESC
) AS rn
FROM sales
) ranked
WHERE rn <= 3;
-- Exactly 3 rows per category (swap in RANK() to include ties,
-- which can return more than 3)
Common Pitfalls
Most ranking bugs come from filtering on the rank in the wrong place, ties you did not plan for, or a forgotten PARTITION BY. Check these four before trusting the output.
- Filtering in the same query:
WHERE rn <= 3fails next to the window function โ window functions run afterWHERE. Wrap in a subquery or CTE. - Nondeterministic ROW_NUMBER on ties: tied rows get arbitrary numbers unless you add a tiebreaker like
ORDER BY score DESC, id. - RANK gaps surprise reports: after a 2-way tie for 1st, the next rank is 3 โ "rank 2" simply doesn't exist. Use
DENSE_RANKif gaps look wrong. - Missing PARTITION BY: without it the ranking runs over the whole table, so "per group" queries quietly return global ranks.
Pro Tip: Ask "what should happen on a tie?" before picking a function: need exactly N rows โ ROW_NUMBER; ties share a place with gaps โ RANK; counting value levels โ DENSE_RANK. If you're new to OVER(), start with window function basics.