SQL Ranking Functions Demystified: When to Use ROW_NUMBER, RANK, or DENSE_RANK
Ranking is everywhere in analytics.
Every time you build a leaderboard, select top-performing customers, remove duplicate records, or prioritize tasks, you are using ranking logic—sometimes without even realizing it.
SQL gives us three powerful window functions for this purpose:
ROW_NUMBER()
RANK()
DENSE_RANK()
They look almost identical in syntax. But their behavior—especially when ties exist—can completely change your results. Choosing the wrong one can quietly introduce errors into dashboards, reports, or business decisions.
This post breaks down these three ranking functions using a simple dataset, real-world examples, and clear explanations—so you always know which one to use and why.
Why Ranking Functions Matter More Than You Think
Ranking functions are not just for “top 10” lists. They show up in many everyday analytics problems, such as:
Selecting the top N records per group
Identifying duplicate rows
Assigning priority or order
Categorizing performance levels
Building competition-style leaderboards
Creating tiers, bands, or segments
Resolving tie-breaking logic
In real projects, the difference between ROW_NUMBER and RANK might decide:
Who gets a bonus
Which customer is contacted first
Which record survives deduplication
How results appear in executive dashboards
That’s why understanding tie behavior is critical.
The Three Ranking Functions at a Glance
Function | Handles Ties | Behavior | Common Use Case |
ROW_NUMBER | No | Always unique numbers | Deduplication, pagination |
RANK | Yes | Skips numbers after ties | Competitions, awards |
DENSE_RANK | Yes | No gaps after ties | Segmentation, tiering |
This table alone is useful—but the real understanding comes from seeing them in action.
Our Sample Dataset
Imagine we’re working with employee performance data:
employee_id | name | score |
101 | Alice | 95 |
102 | Bob | 90 |
103 | Carol | 95 |
104 | David | 88 |
105 | Emma | 88 |
106 | Frank | 85 |
Notice two important things:
Alice and Carol have the same score
David and Emma also share a score
These ties are exactly where ranking functions start to behave differently.
Applying All Three Ranking Functions
Let’s apply all three ranking functions in a single query:
SELECT
employee_id,
name,
score,
ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
RANK() OVER (ORDER BY score DESC) AS rank_num,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank_num
FROM employee_scores;
This query sorts employees by score (highest first) and assigns three different types of ranks.
Output Comparison
name | score | ROW_NUMBER | RANK | DENSE_RANK |
Alice | 95 | 1 | 1 | 1 |
Carol | 95 | 2 | 1 | 1 |
Bob | 90 | 3 | 3 | 2 |
David | 88 | 4 | 4 | 3 |
Emma | 88 | 5 | 4 | 3 |
Frank | 85 | 6 | 6 | 4 |
At first glance, the numbers look similar. But the logic behind them is very different.
Understanding Each Function
ROW_NUMBER(): Strict Ordering, No Mercy for Ties
ROW_NUMBER() assigns a unique number to every row, no matter what.
Even if two employees have the same score, SQL still forces an order and assigns different numbers.
Think of it as: “Line everyone up and number them one by one.”
Best use cases:
Removing duplicates
Pagination (page 1, page 2, etc.)
Selecting the most recent or first record
Situations where ties should not exist logically
If you need exactly one row per group, ROW_NUMBER() is your best friend.
RANK(): Competition-Style Ranking
RANK() respects ties—but introduces gaps.
When two employees tie for first place, they both get rank 1. The next employee doesn’t get rank 2—they get rank 3.
Think of it as: “How medals are awarded in competitions.”
Best use cases:
Sales contests
Sports leaderboards
Exam or scholarship rankings
Award-based systems
If skipping rank numbers feels natural to the problem, RANK() is usually the right choice.
DENSE_RANK(): Clean and Continuous Ranking
DENSE_RANK() also respects ties—but never skips numbers.
If two employees tie for first, the next rank is 2—not 3.
Think of it as: “Grouping people into performance levels.”
Best use cases:
Performance bands
Risk levels
Customer segmentation
Reporting categories
If skipped numbers would confuse stakeholders, DENSE_RANK() keeps things clean and readable.
Real-World Scenarios You’ll Actually Face
1️⃣ Deduplication with ROW_NUMBER
You want the latest record per customer:
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY updated_at DESC
)
Filter where row_number = 1, and duplicates disappear—cleanly and predictably.
2️⃣ Competitions and Awards with RANK
Two sales reps tie for the highest revenue:
Both get rank 1
The next rep gets rank 3
This mirrors how competitions work in the real world—and avoids unfair rankings.
3️⃣ Performance Bands with DENSE_RANK
You want to group customers into tiers:
High value → Tier 1
Medium value → Tier 2
Low value → Tier 3
This is where DENSE_RANK() shines, especially in dashboards and reports.
Which One Should You Use?
Scenario | Best Function |
Remove duplicates | ROW_NUMBER |
Pick top N per group | ROW_NUMBER |
Leaderboards with ties | RANK |
Ranking without gaps | DENSE_RANK |
Grouping similar values | DENSE_RANK |
Final Thoughts
ROW_NUMBER, RANK, and DENSE_RANK may look similar, but they solve very different problems.
ROW_NUMBER → unique ordering, no ties
RANK → ties allowed, gaps included
DENSE_RANK → ties allowed, no gaps
Once you understand how each function treats ties, ranking problems stop being confusing—and your SQL becomes more intentional and reliable.
If you work in analytics, reporting, or data engineering, mastering these three functions is not optional. It’s essential


