SQLab Hub
Lesson 41 · RANK & DENSE_RANK
Lesson 41 · More Topics

SQL RANK() vs DENSE_RANK(): Differences & Examples

RANK() and DENSE_RANK() are window functions that give rows a position in an ordered list, and rows with equal values share the same position. After a tie, RANK() skips numbers (1, 2, 2, 4) while DENSE_RANK() does not (1, 2, 2, 3).

🏅 Ranking with ties ▶ Runnable Playground examples 🧪 4 practice exercises 📝 5-question quiz ⏱ ~15 min

RANK() vs DENSE_RANK() Side by Side

Several managers in the Playground have the same number of direct reports, so ranking managers by team size produces ties. The query below ranks managers by team size with all three ranking functions:

SQLite · runs in the Playground
WITH team AS (
  SELECT manager_id, COUNT(*) AS reports
  FROM employees
  WHERE manager_id IS NOT NULL
  GROUP BY manager_id
)
SELECT manager_id, reports,
       ROW_NUMBER() OVER (ORDER BY reports DESC, manager_id) AS row_num,
       RANK()       OVER (ORDER BY reports DESC) AS rnk,
       DENSE_RANK() OVER (ORDER BY reports DESC) AS dense_rnk
FROM team
ORDER BY reports DESC, manager_id
LIMIT 8;

▶ Run this query in the SQL Playground

Managers ranked by number of direct reports with ROW_NUMBER, RANK and DENSE_RANK
manager_idreportsrow_numrnkdense_rnk
914111
213222
713322
1013422
812553
111664
311764
411864
✓ 8 rows · real output from the Playground database

Read down the three columns. Wherever two managers have the same number of reports, rnk and dense_rnk repeat a value while row_num keeps counting. After the tie, rnk jumps ahead to match the row position and dense_rnk simply continues with the next number.

FunctionTied rowsAfter a tieValues 90, 80, 80, 70
ROW_NUMBER()Different numbersKeeps counting1, 2, 3, 4
RANK()Same numberSkips: the next rank is the row position1, 2, 2, 4
DENSE_RANK()Same numberNo gap: the next rank is the next integer1, 2, 2, 3

Syntax

SQL · syntax
RANK()       OVER ([PARTITION BY group_column] ORDER BY sort_column [DESC])
DENSE_RANK() OVER ([PARTITION BY group_column] ORDER BY sort_column [DESC])

Both functions take no arguments. The ORDER BY inside OVER is what defines the ranking: rows with the same ORDER BY values are ties. They are part of the window functions family and work in PostgreSQL, SQL Server, Oracle, MySQL 8.0+ and SQLite 3.25+.

Ranking Within Groups

Add PARTITION BY to rank inside each group. This ranks products by rating within their category:

SQLite · runs in the Playground
SELECT category, name, rating,
       RANK() OVER (PARTITION BY category ORDER BY rating DESC) AS rating_rank
FROM products
WHERE category IN ('Books', 'Toys')
ORDER BY category, rating_rank, name
LIMIT 10;

▶ Run this query in the SQL Playground

Products ranked by rating within each category
categorynameratingrating_rank
BooksThe Manager's Path51
BooksThe Manager's Path4.92
BooksThe Pragmatic Programmer4.92
BooksZero to One (Thiel)4.92
BooksSystem Design Interview Vol.24.75
BooksThe Manager's Path4.75
BooksDeep Work (Newport)4.67
BooksSystem Design Interview Vol.24.38
BooksDeep Work (Newport)49
BooksSystem Design Interview Vol.23.910
✓ 10 rows · real output from the Playground database

The rank starts again at 1 for each category, and products with the same rating share a rank.

Finding the Nth Highest Value

"Find the second (or third) highest salary" is a classic interview question. DENSE_RANK() answers it directly, because its ranks count distinct values:

SQLite · runs in the Playground
WITH ranked AS (
  SELECT first_name, last_name, salary,
         DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
  FROM employees
)
SELECT first_name, last_name, salary
FROM ranked
WHERE rnk = 3;

▶ Run this query in the SQL Playground

Employees with the third highest salary
first_namelast_namesalary
JohnLee203467
✓ 1 row · real output from the Playground database

Use DENSE_RANK() here rather than RANK(). If two people shared the top salary, RANK() would number the rows 1, 1, 3 and "rank 2" would not exist at all.

Top N per Group, Keeping Ties

When everyone tied for a place should be included, filter on a rank instead of a row number. This returns each department's top two salary levels:

SQLite · runs in the Playground
WITH ranked AS (
  SELECT department, first_name, last_name, salary,
         DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rnk
  FROM employees
)
SELECT department, first_name, last_name, salary, rnk
FROM ranked
WHERE rnk <= 2
ORDER BY department, rnk, last_name
LIMIT 8;

▶ Run this query in the SQL Playground

Employees in the top two salary levels of each department
departmentfirst_namelast_namesalaryrnk
Customer SuccessNancyThompson2039991
Customer SuccessMariaGarcia1911202
DesignAmyNguyen1945431
DesignJoshuaHall1916572
EngineeringDonnaJohnson1990291
EngineeringDorothyMartinez1956902
FinancePaulRobinson1960321
FinanceLindaMartinez1946042
✓ 8 rows · real output from the Playground database

Which One Should You Use?

You wantUse
Exactly N rows per group, ties broken arbitrarily or by a tie-breakerROW_NUMBER()
Competition-style places, where two golds mean no silverRANK()
The Nth distinct value, or the top N value levelsDENSE_RANK()

Common Ranking Mistakes

Three errors that are easy to miss.

1. Using RANK() to find the Nth highest value

❌ Rank 2 may not exist
SQL
RANK() OVER (ORDER BY salary DESC) AS rnk
-- then WHERE rnk = 2
✅ Counts distinct values
SQL
DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
-- then WHERE rnk = 2

With a tie for first place, RANK() produces 1, 1, 3. Filtering for rank 2 then returns nothing.

2. Ranking in the wrong direction

❌ Rank 1 is the lowest salary
SQL
RANK() OVER (ORDER BY salary)
✅ Rank 1 is the highest
SQL
RANK() OVER (ORDER BY salary DESC)

ORDER BY is ascending by default, so the smallest value gets rank 1. Add DESC when the best is the biggest.

3. Filtering on the rank in WHERE

❌ Error: window function in WHERE
SQL
SELECT name,
       RANK() OVER (ORDER BY price DESC) AS rnk
FROM products
WHERE rnk <= 3;
✅ Rank in a CTE, filter outside
SQL
WITH ranked AS (
  SELECT name,
         RANK() OVER (ORDER BY price DESC) AS rnk
  FROM products
)
SELECT name, rnk
FROM ranked
WHERE rnk <= 3;

Window functions are calculated after WHERE. Compute the rank first, then filter in an outer query.

Practice RANK & DENSE_RANK Queries

4 exercises on the real Playground tables.

Write each query yourself first. Every solution has been run against the Playground database.

  1. 1Easy Rank products by review_count, highest first, with RANK(). Show the first eight rows. Table: products

    Show solution
    SQLite · runs in the Playground
    SELECT name, review_count,
           RANK() OVER (ORDER BY review_count DESC) AS rnk
    FROM products
    ORDER BY rnk, name
    LIMIT 8;

    ▶ Run this query in the SQL Playground

    Products with the same review_count share a rank.

  2. 2Medium Find the employees with the second highest salary in the company. Table: employees

    Show solution
    SQLite · runs in the Playground
    WITH ranked AS (
      SELECT first_name, last_name, salary,
             DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
      FROM employees
    )
    SELECT first_name, last_name, salary
    FROM ranked
    WHERE rnk = 2;

    ▶ Run this query in the SQL Playground

    DENSE_RANK() counts distinct salary values, so rank 2 is always the second highest value.

  3. 3Medium Rank shipping cities by their number of orders and show the top eight. Table: orders

    Show solution
    SQLite · runs in the Playground
    SELECT shipping_city, COUNT(*) AS orders,
           RANK() OVER (ORDER BY COUNT(*) DESC) AS rnk
    FROM orders
    GROUP BY shipping_city
    ORDER BY rnk, shipping_city
    LIMIT 8;

    ▶ Run this query in the SQL Playground

    A window function can rank the result of GROUP BY: the aggregate is calculated first and then ranked.

  4. 4Challenging For each department, return the employees with the highest salary, including ties. Table: employees

    Show solution
    SQLite · runs in the Playground
    WITH ranked AS (
      SELECT department, first_name, last_name, salary,
             RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rnk
      FROM employees
    )
    SELECT department, first_name, last_name, salary
    FROM ranked
    WHERE rnk = 1
    ORDER BY department;

    ▶ Run this query in the SQL Playground

    Filtering on rank 1 keeps every employee who shares the top salary in a department: 10 rows.

📝 Quiz — RANK & DENSE_RANK

Test your understanding · 0/5 answered

Question 1 of 5

Four rows have the scores 50, 40, 40, 30 (ranked highest first). What does RANK() return?

Question 2 of 5

For the same scores, what does DENSE_RANK() return?

Question 3 of 5

Which function is the safest choice for "find the second highest salary"?

Question 4 of 5

What makes two rows a tie for RANK()?

Question 5 of 5

What does PARTITION BY do in RANK() OVER (PARTITION BY category ORDER BY price DESC)?

RANK & DENSE_RANK Interview Questions

Q1. What is the difference between RANK() and DENSE_RANK()?

Both give tied rows the same rank. RANK() then skips as many numbers as there were extra tied rows, so ranks can have gaps. DENSE_RANK() continues with the next integer, so its ranks are consecutive.

Q2. How do you find the Nth highest salary?

Compute DENSE_RANK() OVER (ORDER BY salary DESC) in a CTE and select the rows where the rank equals N. It handles ties and works for any N.

Q3. When would you choose ROW_NUMBER() over RANK()?

When you need exactly one row per position, for example exactly three rows per group or one row to keep when removing duplicates. RANK() can return more rows than expected when there are ties.

Frequently Asked Questions

What is the difference between RANK and DENSE_RANK in SQL?

RANK() leaves gaps in the ranking after tied rows (1, 2, 2, 4). DENSE_RANK() does not leave gaps (1, 2, 2, 3). Both give tied rows the same rank.

Does RANK() need ORDER BY?

Yes. The ORDER BY inside the OVER clause defines what is being ranked. Without it every row is treated as tied and receives rank 1.

Can I use RANK() in a WHERE clause?

Not directly. Window functions are calculated after WHERE. Put the query in a CTE or subquery and filter on the rank column in the outer query.

What is the difference between RANK() and ROW_NUMBER()?

ROW_NUMBER() gives every row a different number, even when values are equal. RANK() gives equal values the same number.