SQLQuokka
Start practicing
QuestionsLearnSandboxFeaturesRoadmapFAQ
SQL Concept

SQL ROW_NUMBER vs RANK vs DENSE_RANK

All three functions number rows in a chosen order, and they only differ when values tie. That difference decides whether "top 3 salaries" returns three rows, four rows or skips a rank, so it comes up in almost every window-function interview.

Examples run in MySQL 8 and PostgreSQL · Updated

In short

  • ROW_NUMBER: 1, 2, 3, 4, always unique.
  • RANK: ties share a rank, then it skips (1, 1, 3).
  • DENSE_RANK: ties share a rank, no gaps (1, 1, 2).
  • PARTITION BY restarts the numbering for each group.

How window functions work

A window function computes a value for each row from a "window" of related rows, without collapsing them like GROUP BY does. OVER (PARTITION BY dept_id ORDER BY salary DESC) means: within each department, ordered by salary from highest.

In Sales, Meera and Arjun both earn 64,000. RANK and DENSE_RANK give them both 1. ROW_NUMBER must still give 1 and 2, so add a tie-breaker (here name) to make that choice predictable.

Which one to use

Use ROW_NUMBER for exactly one row per group (deduplication, latest record). Use DENSE_RANK for "top N distinct values" such as the top three salary levels. Use RANK when you need competition-style ranking, where ties push later ranks down.

The example tables

Every example on this page uses these tables. They are small on purpose, so you can check each result by eye.

employees

emp_idnamedept_idsalarymanager_idhire_date
1Aarav1095000NULL2021-03-01
2Diya108800012022-06-15
3Kabir107200012023-01-10
4Meera2064000NULL2020-11-20
5Rohan205800042024-02-05
6Isha3061000NULL2022-09-12
7Arjun206400042023-07-30
8ZoyaNULL52000NULL2024-05-01

Examples

All three side by side

Sales only, so the tie is easy to see.

SELECT name, dept_id, salary,
       ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC, name) AS row_num,
       RANK()       OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rnk,
       DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS dense_rnk
FROM employees
WHERE dept_id = 20
ORDER BY salary DESC, name;
Result
namedept_idsalaryrow_numrnkdense_rnk
Arjun2064000111
Meera2064000211
Rohan2058000332

Gaps vs no gaps

Across the whole company, after the 64,000 tie RANK jumps to 6 while DENSE_RANK continues at 5.

SELECT name, salary,
       RANK() OVER (ORDER BY salary DESC) AS rnk,
       DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnk
FROM employees
ORDER BY salary DESC, name;
Result
namesalaryrnkdense_rnk
Aarav9500011
Diya8800022
Kabir7200033
Arjun6400044
Meera6400044
Isha6100065
Rohan5800076
Zoya5200087

Top earner per department

Number rows inside each department, then keep row 1. The filter has to happen outside, in a CTE.

WITH ranked AS (
  SELECT name, dept_id, salary,
         ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC, name) AS rn
  FROM employees
  WHERE dept_id IS NOT NULL
)
SELECT dept_id, name, salary
FROM ranked
WHERE rn = 1
ORDER BY dept_id;
Result
dept_idnamesalary
10Aarav95000
20Arjun64000
30Isha61000

Common mistakes

  • ROW_NUMBER when ties should count. "Highest-paid employees per department" usually means everyone tied at the top. Use RANK or DENSE_RANK then.
  • No tie-breaker. With ties, ROW_NUMBER picks an arbitrary order unless you add a second ORDER BY column.
  • Filtering in the same SELECT. You cannot write WHERE rn = 1 next to the window function. Wrap it in a CTE or subquery.

Practise ROW_NUMBER vs RANK vs DENSE_RANK

Interview questions that use this concept, easiest first. Each one has the step-by-step visualizer and answer checking.

  1. Rank Salaries Within Department

    Assign dense rank values to employee compensations per department without skipping ranks for tied salaries.

    Medium✓ Solved
  2. Month-over-Month Revenue Growth

    Compute monthly successful payment volume and its percentage change versus the prior month.

    Medium✓ Solved
  3. Running Total of Daily Sales

    Calculate a cumulative running balance of transaction amounts partitioned by retail store location.

    Medium✓ Solved
  4. Amazon: First Order Details

    Retrieve each customer's very first order, resolving date ties by selecting the lowest order_id.

    Medium✓ Solved
  5. Amazon: Days Between Repeat Orders

    Calculate the average gap in days between consecutive purchases for customers with repeat orders.

    Medium✓ Solved
  6. Amazon: Best-Selling Products per Category

    Identify the top-selling product in each retail category by total units sold, returning all tied leaders using DENSE_RANK.

    Medium✓ Solved

Questions people ask

Does MySQL support window functions?
Yes, since MySQL 8.0. PostgreSQL has supported them for much longer. The syntax shown here works in both.
What does PARTITION BY do?
It splits rows into groups and restarts the numbering in each group, while keeping every row in the result.