🔢 Ranks in Order: Understanding ROW_NUMBER(), RANK(), and DENSE_RANK()

This guide explains how to use SQL ranking functions—ROW_NUMBER(), RANK(), and DENSE_RANK()—to assign positions to rows within partitions. You'll learn how each function handles ties and how to apply them in ordered data sets.


🚀 Ranking Like a Pro: Why These Functions Matter

"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."


🤔 What Are ROW_NUMBER(), RANK(), and DENSE_RANK()?

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

🔧 Syntax Snapshot

SQL window function syntax template using OVER() with optional PARTITION BY and ORDER BY clauses. Applies to functions like RANK(), SUM(), or FIRST_VALUE().
FUNCTION() OVER (
  PARTITION BY some_column
  ORDER BY sort_column [ASC|DESC]
)

Use the appropriate function depending on your business rule for ranking.


📋 See It in Action

Let's use the same dataset and apply all three ranking functions in a single query. This makes their differences crystal clear:

🧾 Sample Data

CustomerID Region OrderAmount
C1 East 500
C2 East 700
C3 East 700
C4 East 600

🔎 Query with All Three Functions

SQL example: Compare ROW_NUMBER(), RANK(), and DENSE_RANK() by assigning order rankings within each region based on descending order amount.
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;

📊 Result

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.

🧭 When Should You Use Each?

  • Use ROW_NUMBER() when every row needs a unique position, even with ties
  • Use RANK() when ties should share a position but leave gaps
  • Use DENSE_RANK() when ties share position but no gaps are allowed

🧠 Keywords That Hint You're Ranking

  • "Top N"
  • "Leaderboard"
  • "Position"
  • "Sorted within each group"
  • "Break ties"

⚠️ Common Pitfalls

  • ❌ Assuming RANK() and ROW_NUMBER() behave the same
  • ❌ Forgetting ORDER BY in the window clause—without it, ranking is meaningless
  • ❌ Using RANK() when DENSE_RANK() is required for compact numbering

🔄 CTEs and Filtering for Top Results

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:

  • Return only the top performer in each category
  • Get the most recent event per group
  • Limit results after ranking

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;

➕ Combine with Aggregates or Analytics

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:

SQL example: Use RANK() to identify the top 5 spenders per region, then filter and aggregate based on that ranking for “Top N” analysis.
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 Tips

Debugging ranking queries often means checking how ordering and ties behave under the hood.

  • Add ORDER BY and PARTITION BY one at a time to isolate issues
  • Use ROW_NUMBER() to visually trace sort order
  • Ties are based on all ORDER BY columns, not just the first
  • For 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:

    SQL example: Use ROW_NUMBER() to assign a unique rank to each customer within a region, sorted by descending order amount and customer ID.
    ROW_NUMBER() OVER (PARTITION BY Region ORDER BY OrderAmount DESC, CustomerID)
    

    This ensures consistent, repeatable row numbering in the presence of ties.


🔗 Related Topics

(See also: NTILE(), Aggregate Window Functions, Filtering with CTEs)

Bootstrap JS is working!