Curious about the difference between SQL RANK vs DENSE_RANK vs ROW_NUMBER? This guide explains these SQL ranking functions step by step — how they assign row positions, handle ties, and appear in real-world ROW_NUMBER SQL examples. By the end, you’ll understand exactly when to use each and how to tackle RANK vs DENSE_RANK interview questions with confidence.
This tutorial compares SQL Rank vs RowNumber vs DenseRank side by side, showing how each ranking function behaves in queries with ties, partitions, and ordering.
"Xorthax wants to reward the top traders in each star cluster—but what if multiple customers have the same sales? Should they tie? Skip numbers? SQL ranking functions let us handle it with precision."
These are ranking window functions that assign an ordered position to each row within a partition. They differ in how they handle ties (rows with equal values).
These three functions are often compared together — many developers search for SQL RANK vs DENSE_RANK vs ROW_NUMBER because they behave differently when ties occur.
| Function | Handles Ties? | Behavior |
|---|---|---|
ROW_NUMBER() |
❌ | Always assigns unique rank |
RANK() |
✅ | Tied rows get same rank, skip gaps |
DENSE_RANK() |
✅ | Tied rows get same rank, no gaps |
In some articles or interview guides, you might see SQL Row_number written without the underscore — it refers to the same function shown here, ROW_NUMBER().
OVER() clause defines how rows are grouped and ordered — using PARTITION BY to divide data into groups and ORDER BY to sort rows.
You’ll use this structure for functions like RANK(), ROW_NUMBER(), DENSE_RANK(), SUM(), or FIRST_VALUE().
FUNCTION() OVER (
PARTITION BY some_column
ORDER BY sort_column [ASC|DESC]
)
All three — SQL Rank, SQL Row_number, and SQL DenseRank — follow the same OVER() window clause pattern, differing only in how they handle ties.
Choose the ranking function that best matches your goal:
ROW_NUMBER() for unique ordering, RANK() when ties should leave gaps, and DENSE_RANK() when ties should stay consecutive.
⚠️ Non-deterministic results:
If two rows have the same value in the ORDER BY column, ROW_NUMBER() may label them differently on each run.
Add a tie-breaker column to the ORDER BY for consistent results.
Let’s look at some ROW_NUMBER SQL examples and see how they differ from RANK and DENSE_RANK in the same query.
| CustomerID | Region | OrderAmount |
|---|---|---|
| C1 | East | 500 |
| C2 | East | 700 |
| C3 | East | 700 |
| C4 | East | 600 |
SELECT CustomerID, Region, OrderAmount,
ROW_NUMBER() OVER (PARTITION BY Region ORDER BY OrderAmount DESC) AS RowNum,
RANK() OVER (PARTITION BY Region ORDER BY OrderAmount DESC) AS Rank,
DENSE_RANK() OVER (PARTITION BY Region ORDER BY OrderAmount DESC) AS DenseRank
FROM Orders;
| CustomerID | OrderAmount | RowNum | Rank | DenseRank |
|---|---|---|---|---|
| C2 | 700 | 1 | 1 | 1 |
| C3 | 700 | 2 | 1 | 1 |
| C4 | 600 | 3 | 3 | 2 |
| C1 | 500 | 4 | 4 | 3 |
ROW_NUMBER() gives every row a unique position.RANK() assigns the same rank to ties and skips numbers after.DENSE_RANK() assigns the same rank to ties but does not skip ranks.Notice how each function handles ties differently:
ROW_NUMBER() keeps counting with no regard for duplicates.RANK() ties but leaves gaps.DENSE_RANK() ties but keeps numbering compact.ROW_NUMBER() when every row needs a unique position, even with tiesRANK() when ties should share a position but leave gapsDENSE_RANK() when ties share position but no gaps are allowedUnderstanding when to use each function is a common SQL ranking functions interview question, especially when comparing RANK vs DENSE_RANK.
RANK() and ROW_NUMBER() behave the sameORDER BY in the window clause—without it, ranking is meaninglessRANK() when DENSE_RANK() is required for compact numberingROW_NUMBER() is stable when values tie in the ORDER BY — it can assign different numbers to tied rows unless you include a secondary sort column.Many learners confuse RANK vs DENSE_RANK in SQL — it’s a subtle distinction that often shows up in interview questions and code challenges.
Ranking functions become especially powerful when paired with CTEs (Common Table Expressions). CTEs allow you to assign ranks and then filter the results in a clean, readable way.
This is useful when you're asked to:
Use a structure like this to select only the top-ranked rows per partition:
WITH RankedOrders AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY Region ORDER BY OrderAmount DESC) AS RowNum
FROM Orders
)
SELECT * FROM RankedOrders WHERE RowNum = 1;
Ranking functions can be used as a first step to filter data before applying other analytics. This is especially helpful in "Top N" style analyses, where we want to consider only the best performers.
In the example below, we first rank all customers by total spending within each region. Then we filter to keep only the top 5 spenders per region, and finally use an aggregate function to summarize their behavior:
WITH TopSpenders AS (
SELECT CustomerID, Region, TotalAmount,
RANK() OVER (PARTITION BY Region ORDER BY TotalAmount DESC) AS SpendRank
FROM CustomerTotals
)
SELECT Region, CustomerID, TotalAmount
FROM TopSpenders
WHERE SpendRank <= 5
ORDER BY Region, SpendRank;
Debugging ranking queries often means checking how ordering and ties behave under the hood.
ORDER BY and PARTITION BY one at a time to isolate issuesROW_NUMBER() to visually trace sort orderORDER BY columns, not just the firstFor ROW_NUMBER(), if two rows have the same value in the ORDER BY, SQL uses internal row order to sort the order which may seem arbitrary. If you wish to avoid this, specify a secondary column to break the tie:
ROW_NUMBER() OVER (PARTITION BY Region ORDER BY OrderAmount DESC, CustomerID)
This ensures consistent, repeatable row numbering in the presence of ties.
Mastering SQL Rank vs Row_number vs DenseRank is one of the fastest ways to improve your analytic SQL skills and perform confidently in data interviews.
See also: NTILE(), Aggregate Window Functions, Filtering with CTEs