SQL Practice

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 shows up alongside RANK in 11% of verified SQL interview questions in our corpus. Interviewers love the Nth highest value prompt because the wrong ranking function gives you the wrong answer with zero error messages. This page covers syntax, the failure modes that hit production, and four interview questions pulled from phone screens.

11%

RANK / DENSE_RANK questions

3

Months to detect the bug

1

Word to fix it

0

Error messages when wrong

Source: DataDriven analysis of 1,042 verified data engineering interview rounds.

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'll spend an hour figuring out why rank 1 only appears in one department. Ask anyone who's written a Looker tile at 11pm.

-- 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 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.

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.

Same data, three functions

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

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.

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;

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.

Nth Highest Value

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. RANK would also assign rank 2 here, but for larger N values, the gap behavior becomes problematic. ROW_NUMBER would assign different ranks to the tied rows, potentially returning the wrong salary.

-- 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)

Top-N Distinct Values per Group

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. This distinction between 'top N values' and 'top N rows' is a frequent interview trap.

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;

Percentile Boundaries Without Gaps

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. This is useful for pricing tiers, performance bands, or any classification where the tier number itself carries meaning and gaps would confuse downstream consumers.

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

DENSE_RANK and NULL Values

NULLs are treated as equal to each other for ranking purposes. All NULL values in the ORDER BY column receive the same DENSE_RANK. Where NULLs land depends on the engine and sort direction. PostgreSQL defaults to NULLS LAST for ASC and NULLS FIRST for DESC. SQL Server places NULLs first for ASC.

-- 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.

What they test:

This is the DENSE_RANK signature question. The interviewer specifically mentions ties to see if you reach for DENSE_RANK instead of ROW_NUMBER. ROW_NUMBER would give different ranks to tied employees and might return the wrong salary as 'second highest.' RANK would work for this specific case but DENSE_RANK is the cleaner choice.

Approach:

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.

What they test:

Conceptual clarity. The interviewer wants to hear about gaps. RANK leaves a gap after tied rows; DENSE_RANK does not. The example should have at least one tie so the difference is visible. A strong answer also explains when the gap matters (filtering by rank number) vs when it does not (just displaying the rank).

Approach:

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.

What they test:

This tests whether you recognize that MAX(DENSE_RANK()) gives the count of distinct values. Since DENSE_RANK produces contiguous integers starting at 1, the maximum rank equals the number of distinct values. This is an alternative to COUNT(DISTINCT price) that works in contexts where COUNT DISTINCT is not available (like some window function scenarios).

Approach:

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.

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.

What they test:

The phrase 'no rank numbers should be skipped' is the direct signal for DENSE_RANK. If a candidate uses RANK instead, the output will have gaps after ties, violating the requirement. This tests whether you read the question carefully and pick the right function.

Approach:

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.

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

Pick the wrong function and the query runs fine, returns fewer rows than the interviewer expected, and nobody catches it for weeks. Drill the three functions until the choice is reflex.