"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).
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 |
FUNCTION() OVER (
PARTITION BY some_column
ORDER BY sort_column [ASC|DESC]
)
Use the appropriate function depending on your business rule for ranking.
Let's use the same dataset and apply all three ranking functions in a single query. This makes their differences crystal clear:
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 allowedRANK()
and ROW_NUMBER()
behave the sameORDER BY
in the window clauseβwithout it, ranking is meaninglessRANK()
when DENSE_RANK()
is required for compact numberingRanking 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 RankNum
FROM Orders
)
SELECT * FROM RankedOrders WHERE RankNum = 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.
(See also: NTILE(), Aggregate Window Functions, Filtering with CTEs)