Ranking and Ties in SQL
ROW_NUMBER, RANK, and DENSE_RANK agree until two rows tie. Then each follows a different rule, and picking the wrong one changes the answer.
Lesson 11.4 · Window functions
numbered rows with
row_number(). Two other functions number rows too, and the three give identical answers
until two rows tie. Then each follows its own rule, and the rule you pick
changes the result.
Three rules for a tie
-
row_number()gives every row a different number. Tied rows are numbered in the order of the window'sORDER BY, so without a tie-breaker the order among them is up to the engine. -
rank()gives tied rows the same number, then skips: 1, 2, 2, 4. It's how a race ranks a tie for second. -
dense_rank()gives tied rows the same number and doesn't skip: 1, 2, 2, 3. The numbers count distinct values, not rows.
select player,
score,
row_number() over (order by score desc, player) as row_num,
rank() over (order by score desc) as score_rank,
dense_rank() over (order by score desc) as dense_score_rank
from results Ranking grouped rows
Windows run after
GROUP BY, so a window can order by an aggregate. In this lesson
count(*)
is each customer's order count, and
rank() over (order by count(*) desc)
ranks the customers by it in the same query.
Which one to use
Use
row_number()
when you need exactly N rows, such as one row per customer or the top 3 per
category, and choose the tie-breaker on purpose. Use
rank()
when ties should share a place and the places after them should reflect how
many rows came first. Use
dense_rank()
for "the second-highest value", where you mean the second distinct value.
Filtering on
rank() = 1
to get one row per group is a common bug; see
why rank() = 1 returns more than one row.
A fourth function,
ntile(n), splits the ordered rows into n groups as evenly as it can, which is how
quartiles and deciles are usually built.
Schema · Garden Shop Tables · customers, orders 2 tables
One row per customer. Some customers have no phone on file.
One row per order. Unshipped orders have a null shipped_date.
Rank the customers who have placed orders by how many orders they placed.
Return customer_id, first_name,
order_count, and three rankings of order_count
from most to fewest: row_num from row_number()
(break ties by customer_id), order_rank from
rank(), and dense_order_rank from
dense_rank(). Sort by order_count descending, then
customer_id.
Run your query to inspect the result.
Expected answer
- Columns: customer_id, first_name, order_count, row_num, order_rank, dense_order_rank.
- Rows: the 16 customers who have ordered, busiest first, ties by customer_id.
- Six customers tie on 2 orders and all get order_rank 2. The next customer gets 8 from rank() and 3 from dense_rank().
Related
OVER, PARTITION BY, ranking, offsets, and running totals.
Ranking, LAG/LEAD, and running totals at a glance.
Practice rank functions, tie handling, and top-N per group.
Tied rows share rank 1; break the tie on purpose.
Median, percentiles, standard deviation, mode, and histograms.
Median, percentiles, spread, mode, and histogram recipes.