SQL Tutorial › Window Functions › ROW_NUMBER, RANK, DENSE_RANK

SQL ROW_NUMBER, RANK and DENSE_RANK

AdvancedLesson 46 of 6911 min read7 queries run on MySQL 8.4

ROW_NUMBER, RANK and DENSE_RANK all number rows in order, and they differ only when there are ties. Interviewers love that difference, and real reports depend on it: should two people with the same score share second place, and is the next one third or fourth? This lesson shows all three side by side on data with a tie.

Advertisement
In this lesson
  • The exact difference between ROW_NUMBER, RANK and DENSE_RANK
  • Rank inside groups with PARTITION BY
  • Find the Nth highest value correctly with ties
  • Split rows into buckets with NTILE

Sample data

Priya and Karan both earn 72,000: the tie that makes the functions differ.

employees · 12 rows (some columns)
emp_idfirst_namedept_idsalary
100MeeraNULL250000
101Asha1125000
102Rahul198000
103Priya172000
104Karan172000
105Vikram2105000
106Neha254000
107Arjun248000
108Kavya388000
109Rohan345000
110Sneha478000
111Amit125000

The three functions side by side

SQL
SELECT first_name, salary,       ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,       RANK()       OVER (ORDER BY salary DESC) AS rnk,       DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnkFROM employeesORDER BY salary DESC;
ResultMySQL 8.4
first_namesalaryrow_numrnkdense_rnk
Meera250000111
Asha125000222
Vikram105000333
Rahul98000444
Kavya88000555
Sneha78000666
Priya72000777
Karan72000877
Neha54000998
Arjun4800010109
Rohan45000111110
Amit25000121211
12 rows in set
How this query works, step by step
  1. All three order the rows by salary, highest first.
  2. ROW_NUMBER gives every row a unique number: the tied pair gets 7 and 8 in an arbitrary order.
  3. RANK gives tied rows the same rank (7) and then skips: the next salary is ranked 9, like positions in a race.
  4. DENSE_RANK gives tied rows the same rank (7) and does not skip: the next is 8.
FunctionTies getAfter a tieUse for
ROW_NUMBER()different numberscontinuesunique numbering, pagination, de-duplication
RANK()the same numberskips (1, 2, 2, 4)competition ranking
DENSE_RANK()the same numberno gap (1, 2, 2, 3)“Nth highest value” questions
Watch out

With ties, ROW_NUMBER decides the order of tied rows arbitrarily. Add a tie-breaker, such as ORDER BY salary DESC, emp_id, when the numbering must be repeatable.

Rank inside each group

PARTITION BY restarts the numbering for each department:

SQL
SELECT dept_id, first_name, salary,       DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rank_in_deptFROM employeesWHERE dept_id IS NOT NULLORDER BY dept_id, rank_in_dept;
ResultMySQL 8.4
dept_idfirst_namesalaryrank_in_dept
1Asha1250001
1Rahul980002
1Priya720003
1Karan720003
1Amit250004
2Vikram1050001
2Neha540002
2Arjun480003
3Kavya880001
3Rohan450002
4Sneha780001
11 rows in set
How this query works, step by step
  1. Each department is ranked separately; every department’s top earner gets 1.
  2. In Engineering, Priya and Karan share rank 3 and Amit is 4.

Nth highest salary

The interview classic: the third highest distinct salary. With DENSE_RANK, a tie does not swallow a position:

SQL
WITH ranked AS (  SELECT first_name, salary,         DENSE_RANK() OVER (ORDER BY salary DESC) AS dr  FROM employees)SELECT first_name, salaryFROM rankedWHERE dr = 3;
ResultMySQL 8.4
first_namesalary
Vikram105000
1 row in set
How this query works, step by step
  1. The distinct salaries in order are 250,000, 125,000, 105,000, 98,000…
  2. The third distinct salary is 105,000, and dr = 3 returns the person who earns it: Vikram.
  3. Unlike LIMIT 1 OFFSET 2, this returns every person at that salary if several share it.

Second highest salary per department:

SQL
WITH ranked AS (  SELECT dept_id, first_name, salary,         DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS dr  FROM employees  WHERE dept_id IS NOT NULL)SELECT dept_id, first_name, salaryFROM rankedWHERE dr = 2ORDER BY dept_id;
ResultMySQL 8.4
dept_idfirst_namesalary
1Rahul98000
2Neha54000
3Rohan45000
3 rows in set

HR has only one employee, so it has no second salary and does not appear.

Remove duplicates with ROW_NUMBER

Keep only the first row of each duplicate group. Here, one employee per salary value (the lowest emp_id):

SQL
WITH numbered AS (  SELECT emp_id, first_name, salary,         ROW_NUMBER() OVER (PARTITION BY salary ORDER BY emp_id) AS rn  FROM employees)SELECT emp_id, first_name, salaryFROM numberedWHERE rn = 1ORDER BY salary DESC;
ResultMySQL 8.4
emp_idfirst_namesalary
100Meera250000
101Asha125000
105Vikram105000
102Rahul98000
108Kavya88000
110Sneha78000
103Priya72000
106Neha54000
107Arjun48000
109Rohan45000
111Amit25000
11 rows in set
How this query works, step by step
  1. PARTITION BY salary groups rows with the same salary.
  2. ROW_NUMBER numbers them 1, 2, … by emp_id.
  3. Keeping rn = 1 keeps one row per salary: Karan (104) is dropped because Priya (103) comes first.
  4. To actually delete duplicates, delete the rows whose rn > 1.

NTILE: split into buckets

NTILE(n) divides the ordered rows into n groups of (nearly) equal size, for quartiles or “top third”:

SQL
SELECT first_name, salary,       NTILE(4) OVER (ORDER BY salary DESC) AS quartileFROM employeesORDER BY salary DESC;
ResultMySQL 8.4
first_namesalaryquartile
Meera2500001
Asha1250001
Vikram1050001
Rahul980002
Kavya880002
Sneha780002
Priya720003
Karan720003
Neha540003
Arjun480004
Rohan450004
Amit250004
12 rows in set
How this query works, step by step
  1. 12 rows split into 4 buckets of 3.
  2. Quartile 1 holds the top three salaries, quartile 4 the bottom three.
  3. If the rows do not divide evenly, the first buckets get one extra row.

PERCENT_RANK and CUME_DIST

SQL
SELECT first_name, salary,       ROUND(PERCENT_RANK() OVER (ORDER BY salary), 2) AS pct_rank,       ROUND(CUME_DIST()    OVER (ORDER BY salary), 2) AS cumulative_distFROM employeesWHERE dept_id = 1;
ResultMySQL 8.4
first_namesalarypct_rankcumulative_dist
Amit2500000.2
Priya720000.250.6
Karan720000.250.6
Rahul980000.750.8
Asha12500011
5 rows in set
  • PERCENT_RANK = (rank − 1) / (rows − 1): 0 for the lowest, 1 for the highest.
  • CUME_DIST = share of rows with a value less than or equal to the current one.

Exercise: Rank products by price

Rank products by price from the most expensive within each category with RANK(), and show category, product name, price and rank.

Expected resultMySQL 8.4
categoryproduct_namepriceprice_rank
ElectronicsMechanical Keyboard34991
ElectronicsUSB-C Hub19992
ElectronicsWireless Mouse7993
FurnitureStanding Desk189991
FurnitureOffice Chair89992
FurnitureMonitor Arm27993
FurnitureDesk Lamp14994
StationeryNotebook Pack2491
StationeryGel Pens1992
9 rows in set
Hint 1

RANK() OVER (PARTITION BY category ORDER BY price DESC).

Quick quiz

1. Salaries 100, 90, 90, 80. What does RANK() give the 80?

2. Same salaries. What does DENSE_RANK() give the 80?

3. Which function never gives two rows the same number?

4. Which is best for “the 2nd highest distinct salary”?

Ranking FAQ

What is the difference between RANK and DENSE_RANK?

Both give tied rows the same rank. RANK then skips numbers (1, 2, 2, 4); DENSE_RANK continues without gaps (1, 2, 2, 3).

What is the difference between ROW_NUMBER and RANK?

ROW_NUMBER always gives unique, consecutive numbers, even to ties. RANK gives ties the same number.

How do I find the Nth highest salary in SQL?

Use DENSE_RANK() OVER (ORDER BY salary DESC) in a CTE and filter on the rank you need. Add PARTITION BY dept_id for the Nth highest per department.

How do I delete duplicate rows using ROW_NUMBER?

Number the rows of each duplicate group with ROW_NUMBER() OVER (PARTITION BY duplicate_columns ORDER BY id) and delete the rows with a number greater than 1.