SQL DENSE_RANK Explained

An e-commerce BI team shipped a 'top 5 products by revenue' dashboard using RANK and discovered three months later it was showing 4 products on half the pages because two items kept tying at rank 3. The fix was one word: DENSE_RANK. Support tickets stopped the next morning.

DENSE_RANK Syntax

Zero arguments. Mandatory OVER clause. ORDER BY is required or the result is undefined. PARTITION BY is optional, but skip it once in a top-N-per-group query and you will spend an hour figuring out why rank 1 only appears in one department.

DENSE_RANK works in PostgreSQL, MySQL 8+, SQL Server, Oracle, Snowflake, BigQuery, Redshift, and every other modern SQL engine. The syntax is identical across all of them. No vendor-specific variations exist for this function.

Prepare for the interview
01 / Open invite
02min.

Know Window Functions the way the interviewer who asks it knows it.

a Window Functions query, the same shape a screen would give you.
The diff against expected. Where ties broke. What you missed.
sandbox
1SELECT user_id,
2 COUNT(*) AS sessions
3FROM events
4WHERE ts >= NOW() - INTERVAL '7 day'
5
Execute your solution0.4s avg.
LyftInterview question
Solve a Window Functions problem
11%
RANK / DENSE_RANK questions
3
Months to detect the bug
1
Word to fix it
0
Error messages when wrong

When to Use DENSE_RANK

DENSE_RANK shines in three specific scenarios. In all other cases, ROW_NUMBER (for uniqueness) or RANK (for competition-style gaps) is usually a better fit.

Prepare for the interview
03 / From the bank03 of many
03hand-picked.

Top Selling Items

Easy10 min

Revenue crowns the winners. Who sold the most?

DENSE_RANK Syntax

-- Basic syntax
DENSE_RANK() OVER (
  [PARTITION BY partition_column, ...]
  ORDER BY sort_column [ASC|DESC]
)

-- Example: rank employees by salary within each department
SELECT
  department,
  employee_name,
  salary,
  DENSE_RANK() OVER (
    PARTITION BY department
    ORDER BY salary DESC
  ) AS salary_rank
FROM employees;

DENSE_RANK vs RANK vs ROW_NUMBER

The three ranking functions agree on non-tied rows. They only diverge when duplicates appear in the ORDER BY column. This table shows salaries with two pairs of ties so you can see the divergence clearly.

SalaryROW_NUMBERRANKDENSE_RANK
$120,000111
$110,000222
$110,000322
$100,000443
$95,000554
$95,000654
$90,000775

All Three Functions Side by Side

SELECT
  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_rnk
FROM employees
ORDER BY salary DESC;

Key insight: DENSE_RANK produces 5 distinct values for 7 rows. RANK also has 5 distinct values but skips 3 and 6. ROW_NUMBER has 7 unique values with no ties at all. The maximum DENSE_RANK value always equals the count of distinct ORDER BY values.

Nth Highest Value

-- Find the 3rd highest salary
SELECT DISTINCT salary
FROM (
  SELECT
    salary,
    DENSE_RANK() OVER (ORDER BY salary DESC) AS dr
  FROM employees
) ranked
WHERE dr = 3;

-- If salaries are 120k, 110k, 110k, 100k, 95k
-- DENSE_RANK: 1, 2, 2, 3, 4 => dr=3 returns 100k (correct)
-- RANK:       1, 2, 2, 4, 5 => dr=3 returns nothing (gap)
-- ROW_NUMBER: 1, 2, 3, 4, 5 => dr=3 returns 110k (wrong)

The classic interview question: find the 2nd (or Nth) highest salary. DENSE_RANK is the correct choice because it counts distinct values sequentially. If two employees share the top salary, the next distinct salary gets rank 2, not rank 3.

Top-N Distinct Values per Group

WITH ranked AS (
  SELECT
    department,
    salary,
    DENSE_RANK() OVER (
      PARTITION BY department
      ORDER BY salary DESC
    ) AS dr
  FROM employees
)
SELECT DISTINCT department, salary, dr
FROM ranked
WHERE dr <= 3
ORDER BY department, dr;

When the question asks for the top 3 salaries in each department (not the top 3 employees), DENSE_RANK handles duplicates gracefully. Two employees earning the same salary both get the same rank, and the third distinct salary gets rank 3 regardless of how many people share ranks 1 and 2.

Percentile Boundaries Without Gaps

SELECT
  product_id,
  product_name,
  unit_price,
  DENSE_RANK() OVER (ORDER BY unit_price DESC) AS price_tier
FROM products;

-- Tier 1: most expensive
-- Tier 2: second most expensive
-- No gaps even if multiple products share a price

When you need to assign items to numbered tiers based on a metric and want the tier numbers to be contiguous (1, 2, 3, not 1, 2, 5), DENSE_RANK provides that guarantee.

DENSE_RANK and NULL Values

-- PostgreSQL: NULLs sorted to the end for ASC
SELECT
  employee_name,
  bonus,
  DENSE_RANK() OVER (ORDER BY bonus DESC NULLS LAST) AS bonus_rank
FROM employees;

-- Bonuses: 5000, 3000, 3000, NULL, NULL
-- Ranks:   1,    2,    2,    3,    3
-- Both NULLs share rank 3 (treated as equal)

Interview note: If the interviewer asks 'what happens to NULLs in DENSE_RANK,' the correct answer is that all NULLs share one rank. Mention NULLS FIRST/LAST to control placement. Filtering NULLs out before ranking is often the safest approach if NULLs should not participate in the ranking at all.

4 DENSE_RANK Interview Questions

These questions specifically test DENSE_RANK knowledge. Each includes what the interviewer is looking for and the recommended approach.

Q1

Find the second highest salary in each department. If two employees share the highest salary, the second highest should still be identified correctly.

This is the DENSE_RANK signature question. The interviewer specifically mentions ties to see if you reach for DENSE_RANK instead of ROW_NUMBER. Use DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dr, then filter WHERE dr = 2. If the question asks for the employee rows, keep all rows with dr = 2. If it asks for the salary value, add DISTINCT.

Q2

What is the difference between DENSE_RANK and RANK? Give an example where they return different results.

Draw the example: values 100, 90, 90, 80. RANK: 1, 2, 2, 4. DENSE_RANK: 1, 2, 2, 3. The gap in RANK means WHERE rnk = 3 returns nothing. WHERE dr = 3 with DENSE_RANK returns 80. Use DENSE_RANK when you need to address specific rank positions by number.

Q3

Given a products table with categories and prices, find how many distinct price points exist in each category.

Two approaches. Simple: COUNT(DISTINCT price) with GROUP BY category. Window function approach: DENSE_RANK() OVER (PARTITION BY category ORDER BY price) and then take MAX per category. The second approach demonstrates DENSE_RANK understanding, which is what the interviewer wants to see. The maximum DENSE_RANK value equals the count of distinct values.

Q4

Rank students by test score within each class. Students with the same score should have the same rank, and no rank numbers should be skipped.

The phrase 'no rank numbers should be skipped' is the direct signal for DENSE_RANK. Use DENSE_RANK() OVER (PARTITION BY class_id ORDER BY test_score DESC) AS student_rank. The 'no skipped numbers' requirement eliminates RANK. The 'same score same rank' requirement eliminates ROW_NUMBER. Only DENSE_RANK satisfies both.

Which Ranking Function Should You Use?

A quick decision guide for the interview whiteboard.

Need exactly one row per group?

Use ROW_NUMBER(). It guarantees uniqueness. Ties are broken arbitrarily (or by a secondary ORDER BY column you specify).

Ties should share a rank, and gaps are acceptable?

Use RANK(). Two rows at 2nd place means 3rd place does not exist. Like a sports podium.

Ties should share a rank, and rank numbers must be contiguous?

Use DENSE_RANK(). No gaps. The Nth rank always exists if there are at least N distinct values.

DENSE_RANK FAQ

What is DENSE_RANK in SQL?+
DENSE_RANK() is a window function that assigns a rank to each row based on the ORDER BY clause. Rows with identical ORDER BY values receive the same rank. Unlike RANK(), DENSE_RANK() does not leave gaps after ties. If two rows are tied at rank 2, the next row gets rank 3, not rank 4. The result is always a contiguous sequence of integers: 1, 2, 2, 3, 4.
What is the difference between DENSE_RANK and RANK?+
Both assign the same rank to tied rows. The difference is what happens after a tie. RANK skips numbers: two rows at rank 2 means the next row gets rank 4. DENSE_RANK never skips: the next row gets rank 3. Use DENSE_RANK when you need contiguous rank numbers, especially for finding the Nth highest value. Use RANK when you want the ranking to reflect the actual position (like a race where two runners tie for 2nd and there is no 3rd place).
When should I use DENSE_RANK instead of ROW_NUMBER?+
Use DENSE_RANK when tied rows should share the same rank. ROW_NUMBER always assigns unique sequential numbers, so tied rows get different numbers in an arbitrary order. Use ROW_NUMBER for deduplication (where you want exactly one row per group). Use DENSE_RANK when the question involves 'Nth highest value' or 'rank with no gaps' because ties need to be handled explicitly.
Does DENSE_RANK work with PARTITION BY?+
Yes. DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) restarts the ranking at 1 for each department. Each partition gets its own independent, gap-free sequence. Without PARTITION BY, DENSE_RANK ranks all rows as a single group.
02 / Why practice

The Top-5 Dashboard That Only Showed 4. Don't Be That Team.

  1. 01

    Active recall beats re-reading by 50%

    Cognitive-science meta-reviews (Dunlosky et al., 2013) rank practice testing as a top-tier study technique, while re-reading and highlighting rank near the bottom

  2. 02

    76% of hiring managers reject on the coding task, not the resume

    From HackerRank's 2024 Developer Skills Report. Candidates who look strong on paper still fail the live screen if they haven't done timed, executable practice

  3. 03

    Five problem shapes cover 80% of data engineer loops

    Dedup, sessionization, top-N-per-group, slowly-changing dimensions, partition tricks. Writing the shapes by hand turns the unfamiliar into pattern recognition

Related Guides