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.
Know Window Functions the way the interviewer who asks it knows it.
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.
Top Selling Items
Revenue crowns the winners. Who sold the most?
Pulled from debriefs where SQL was the gate.
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.
| Salary | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|
| $120,000 | 1 | 1 | 1 |
| $110,000 | 2 | 2 | 2 |
| $110,000 | 3 | 2 | 2 |
| $100,000 | 4 | 4 | 3 |
| $95,000 | 5 | 5 | 4 |
| $95,000 | 6 | 5 | 4 |
| $90,000 | 7 | 7 | 5 |
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 priceWhen 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.
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.
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.
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.
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?+
What is the difference between DENSE_RANK and RANK?+
When should I use DENSE_RANK instead of ROW_NUMBER?+
Does DENSE_RANK work with PARTITION BY?+
The Top-5 Dashboard That Only Showed 4. Don't Be That Team.
- 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
- 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
- 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