Understanding the difference between rank and dense rank is a fundamental concept for anyone working with ordered data, especially in SQL, business intelligence tools, or data‑analysis programming languages. Both functions assign a sequential number to each row within a partition based on a specified ordering, but they handle ties in distinct ways. Grasping this distinction helps you produce accurate reports, avoid misleading rankings, and choose the right function for your analytical needs.
What Is RANK()?
The RANK() function assigns a rank to each row, but when two or more rows share the same value in the ordering column, they receive the same rank. The next distinct value’s rank is then incremented by the number of rows that tied for the previous rank. Basically, RANK() leaves gaps in the ranking sequence whenever ties occur And it works..
Counterintuitive, but true.
Syntax (SQL‑standard)
RANK() OVER (
PARTITION BY -- optional
ORDER BY [ASC|DESC]
)
Key characteristics
- Tie handling: equal values → identical rank.
- Gap creation: after a tie, the rank jumps by the count of tied rows.
- Use case: when you want to reflect the “position” in a competition where ties skip places (e.g., Olympic medals).
What Is DENSE_RANK()?
The DENSE_RANK() function also assigns the same rank to tied rows, but it does not create gaps. Now, after a set of ties, the next distinct value receives the immediately following integer rank. This means the ranking sequence is dense—there are no missing numbers That's the part that actually makes a difference..
Not the most exciting part, but easily the most useful.
Syntax (SQL‑standard)
DENSE_RANK() OVER (
PARTITION BY -- optional
ORDER BY [ASC|DESC]
)
Key characteristics
- Tie handling: equal values → identical rank (same as RANK()).
- No gaps: ranking continues consecutively regardless of ties.
- Use case: when you need a simple, gap‑free ordering (e.g., tiered loyalty levels, product popularity tiers).
Key Differences Between RANK() and DENSE_RANK()
| Aspect | RANK() | DENSE_RANK() |
|---|---|---|
| Ranking after ties | Skips numbers equal to the count of tied rows | Continues with the next integer |
| Resulting sequence | May contain gaps (e.g., 1, 2, 2, 4, 5) | Always gap‑free (e.g. |
Quick note before moving on.
Understanding these differences ensures you select the function that matches the logical interpretation you need.
Practical Examples
Example Dataset
Consider a table sales with columns salesperson, region, and amount.
| salesperson | region | amount |
|---|---|---|
| Alice | North | 5000 |
| Bob | North | 5000 |
| Carol | North | 3000 |
| Dave | South | 7000 |
| Eve | South | 7000 |
| Frank | South | 4000 |
We want to rank salespeople within each region by amount descending Simple, but easy to overlook. Practical, not theoretical..
Using RANK()
SELECT
salesperson,
region,
amount,
RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS rank_val
FROM sales;
Result
| salesperson | region | amount | rank_val |
|---|---|---|---|
| Alice | North | 5000 | 1 |
| Bob | North | 5000 | 1 |
| Carol | North | 3000 | 3 |
| Dave | South | 7000 | 1 |
| Eve | South | 7000 | 1 |
| Frank | South | 4000 | 3 |
Honestly, this part trips people up more than it should No workaround needed..
Notice the gap after the tie: rank jumps from 1 to 3.
Using DENSE_RANK()
SELECT
salesperson,
region,
amount,
DENSE_RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS dense_rank_val
FROM sales;
Result
| salesperson | region | amount | dense_rank_val |
|---|---|---|---|
| Alice | North | 5000 | 1 |
| Bob | North | 5000 | 1 |
| Carol | North | 3000 | 2 |
| Dave | South | 7000 | 1 |
| Eve | South | 7000 | 1 |
| Frank | South | 4000 | 2 |
Here the ranking is dense: after the tie at rank 1, the next distinct value receives rank 2.
When to Use RANK() vs DENSE_RANK()
| Situation | Prefer RANK() | Prefer DENSE_RANK() |
|---|---|---|
| Competitive standings where ties should leave vacant places (e.g., “1st, 1st, 3rd”) | ✅ | ❌ |
| Tiered classification where you want consecutive levels (e.g. |
In practice, many analysts start with DENSE_RANK() for simplicity and switch to RANK()
ROW_NUMBER(): The Sequential Identifier
While RANK() and DENSE_RANK() handle ties by assigning the same rank, ROW_NUMBER() assigns a unique, sequential integer to every row within the partition, regardless of ties in the ORDER BY column. This makes it invaluable when you need a distinct row identifier for pagination, deduplication, or when the physical order matters more than the logical ranking.
SELECT
salesperson,
region,
amount,
ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS row_num
FROM sales;
Result
| salesperson | region | amount | row_num |
|---|---|---|---|
| Alice | North | 5000 | 1 |
| Bob | North | 5000 | 2 |
| Carol | North | 3000 | 3 |
| Dave | South | 7000 | 1 |
| Eve | South | 7000 | 2 |
| Frank | South | 4000 | 3 |
Even though Alice and Bob have the same amount, they receive different row_num values (1 and 2). The order among tied rows is nondeterministic unless a secondary sort key is specified. To ensure consistent results, you can add a tiebreaker column:
ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC, salesperson ASC)
When to Use ROW_NUMBER()
- Pagination: Fetching a specific page of results (e.g., rows 11–20) requires a stable, unique sequence.
- Deduplication: When you need to keep only one record per group (e.g., the latest transaction per customer), you can filter by
ROW_NUMBER() = 1. - Random Sampling: Assigning a random row number to select a subset of data.
- When Order Within Ties Matters: Take this: alphabetizing salespeople with the same sales volume.
Choosing the Right Function: A Decision Guide
| Use Case | Function | Why |
|---|---|---|
| Olympic medal standings (gold, silver, bronze with ties) | RANK() |
Ties share a rank, and the next rank is skipped (e.And |
| Calculating percentiles with equal-sized buckets | NTILE() |
Divides rows into a specified number of roughly equal groups. g.Practically speaking, |
| Fetching the top N records per group | ROW_NUMBER() |
Guarantees exactly N unique rows, even if there are ties. Which means g. Also, , 1, 1, 3). |
| Product tiers ( Platinum, Gold, Silver) | DENSE_RANK() |
Ties share a rank, and the next rank is consecutive (e., 1, 1, 2). |
| Generating a sequential ID for export or display | ROW_NUMBER() |
Provides a consistent, gap-free numbering independent of data values. |
Advanced Considerations
- Performance: All three functions have similar overhead. The choice should be driven by logic, not speed.
- Window Frame: By default, these functions use
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. For cumulative rankings (e.g., running totals), you might adjust the frame, but for standard ranking, the default is sufficient. - NULLs: By default,
NULLvalues are sorted last inDESCorder and first inASCorder. UseNULLS FIRSTorNULLS LASTto control this behavior explicitly.
Conclusion
Mastering RANK(), DENSE_RANK(), and ROW_NUMBER() is a cornerstone of advanced SQL analytics. The key is to align the function’s behavior with the business semantics of your ranking:
- Use
RANK()when ties should create gaps in the sequence (e.g., competitive standings). - Use
DENSE_RANK()when you want a continuous sequence without gaps (e.g., tiered classifications). - Use
ROW_NUMBER()when you need a unique, arbitrary order for each row (e.g., pagination or deduplication).
By understanding these distinctions, you can transform raw data into meaningful insights, whether you’re building leaderboards, segmenting customers, or preparing data for downstream processing. The window functions toolkit empowers you to answer complex analytical questions with clarity and precision.