Rank vs Dense Rank vs Row Number Window Functions in MySQL
Introduction
In the SQL world, especially in MySQL, window functions are powerful tools for analytics. Among these, RANK(), DENSE_RANK(), and ROW_NUMBER() are three commonly used functions to assign rankings to rows. But they don’t behave the same way — and picking the wrong one can mess up your results.
Let’s break them down so you know exactly when to use each.
Understanding Ranking Functions
Ranking functions in MySQL let you assign a position number to rows within a group, based on the order you define. They’re essential in analytics — from creating leaderboards to analyzing sales performance trends.
They work with the OVER() clause, which defines:
- Partition – how data is grouped.
- Order – the sorting within each partition.
RANK()
Each row in a partition is given a rank using RANK(); however, if two or more rows have the same value, they share the same rank. The next rank gets skipped.
Syntax:
RANK() OVER (PARTITION BY column ORDER BY column)
RANK() Example
Dataset:
Player | Score |
A | 100 |
B | 90 |
C | 90 |
D | 80 |
Query:
SELECT
Player,
Score,
RANK() OVER (ORDER BY Score DESC) AS rank_position
FROM scores;
Output:
Player | Score | rank_position |
A | 100 | 1 |
B | 90 | 2 |
C | 90 | 2 |
D | 80 | 4 |
Key point: Notice rank 3 is missing — because RANK() skips numbers after ties.
DENSE_RANK()
DENSE_RANK() works like RANK() but does not skip numbers after ties. This makes it “dense” — no gaps in ranking.
Syntax:
DENSE_RANK() OVER (PARTITION BY column ORDER BY column)
DENSE_RANK() Example
Using the same dataset:
Query:
SELECT
Player,
Score,
DENSE_RANK() OVER (ORDER BY Score DESC) AS dense_rank_position
FROM scores;
Output:
Player | Score | dense_rank_position |
A | 100 | 1 |
B | 90 | 2 |
C | 90 | 2 |
D | 80 | 3 |
Key point: No missing numbers — ties still share the same rank.
ROW_NUMBER()
ROW_NUMBER() simply gives each row a unique number, even if the values are tied.
Syntax:
ROW_NUMBER() OVER (PARTITION BY column ORDER BY column)
ROW_NUMBER() Example
Query:
SELECT
Player,
Score,
ROW_NUMBER() OVER (ORDER BY Score DESC) AS row_num
FROM scores;
Output:
Player | Score | row_num |
A | 100 | 1 |
B | 90 | 2 |
C | 90 | 3 |
D | 80 | 4 |
Key point: Every row gets a distinct number — ties don’t matter.
Key Differences Between RANK(), DENSE_RANK(), and ROW_NUMBER()
Feature | RANK() | DENSE_RANK() | ROW_NUMBER() |
Handles Ties | Yes | Yes | No |
Skips Numbers | Yes | No | No |
Unique Numbers | No | No | Yes |
Use Case | Leaderboards with gaps | Compact rankings | Sequential numbering |
Performance Considerations
- Large datasets: Ranking functions can be expensive if ordering columns are not indexed.
- Partitioning: Reduces computation but must be chosen wisely.
- Memory usage: The OVER() clause can consume memory proportional to partition size.
Practical Use Cases
- Leaderboards: RANK() or DENSE_RANK() depending on tie handling preference.
- Removing duplicates: ROW_NUMBER() assists in choosing the first occurrence.
- Pagination: Use ROW_NUMBER() to split results into pages.
- Sales analytics: Rank products by revenue or quantity sold.
Common Mistakes to Avoid
- Forgetting ORDER BY — without it, ranking is meaningless.
- Using the wrong ranking function for your needs.
- Over-partitioning — too many partitions slow queries.
Best Practices
- Use RANK() when you want gaps after ties.
- Use DENSE_RANK() when you don’t want gaps.
- Use ROW_NUMBER() for unique numbering regardless of ties.
- Always index your ordering columns for speed.
Conclusion
RANK(), DENSE_RANK(), and ROW_NUMBER() might seem similar at first glance, but their subtle differences can make or break your query logic. Choose based on whether you care about ties, gaps in numbering, or unique sequence assignment.
By mastering these functions, you’ll handle leaderboards, reports, and data cleanup with confidence.
FAQs
Q1: Which is faster, RANK() or ROW_NUMBER()?
A1: Performance is usually similar, but indexing can make ROW_NUMBER() slightly faster in large datasets.
Q2: Can I use these functions without PARTITION BY?
A2: Yes, PARTITION BY is optional. Without it, the ranking applies to the entire result set.
Q3: Does MySQL support NTILE()?
A3: Yes, it does, and it divides data into equal groups — different from ranking.
Q4: How do I reset ranking for each category?
A4: In the OVER() clause, use PARTITION BY category_column.
Q5: Can these be used in UPDATE queries?
A5: Yes, but you must use them in a subquery or CTE first.