Hook: This question is a favorite because one tiny choice—ROW_NUMBER vs DENSE_RANK—can change the answer when salaries tie.
Question: How do I find the Nth highest salary in SQL?
Answer: The usual interview-safe answer is to use a window function and rank salaries in descending order. If duplicate salaries should count as one rank, use DENSE_RANK(); if every row should count separately, use ROW_NUMBER(). Then filter to the row where the rank equals N.
Interview-Ready Answer: I would rank the salaries in descending order with a window function, then filter for rank N. For the common interview version, where duplicate salaries should share the same place, I use DENSE_RANK() so 100, 100, 90 gives ranks 1, 1, 2 instead of skipping numbers. That makes the query correct even when there are ties, and if there are fewer than N distinct salaries, I can return no row or NULL depending on the requirement.
The phrase Nth highest salary sounds simple, but interviewers are checking whether you understand ties and ranking. In most interview settings, highest salary means the distinct salary value at position N, not the Nth employee row. That is why the best tool is usually a window function, which means a function that looks across a set of rows while still returning one result per row.
NULL because a missing salary should not compete with real values.DENSE_RANK() when peers should share the same rank with no gaps. Example: 100, 100, 90 becomes ranks 1, 1, 2.N. If no such rank exists, the result is empty or NULL, depending on how you write the query.DENSE_RANK() is usually the right answer| Function | Ties | Gaps? | Best use |
|---|---|---|---|
ROW_NUMBER() | No sharing | No | Nth row, not Nth distinct salary |
RANK() | Shares rank | Yes | When gaps are okay |
DENSE_RANK() | Shares rank | No | Nth distinct salary |
For this problem, DENSE_RANK() is the cleanest fit because it matches human expectations: equal salaries get equal place, and the next salary gets the next number with no skipped ranks. That is exactly what most interviewers want unless they explicitly say duplicates should count separately.
O(n log n) because the database must sort salaries for the window order. The ranking itself is cheap after sorting.salary can help the optimizer, especially for simple top-N queries, but the engine may still need a full ordered pass for ranking.NULL differently, so do not leave that to chance.NULL. Interviewers often like you to mention this.Memory model: think of the database as lining up salary cards from tallest to shortest. DENSE_RANK() puts the same-height cards on the same step and never leaves empty steps. That is why it is the safest mental model for this question.
Imagine a payroll analytics service inside a large HR platform. Finance wants a report showing the 5th highest salary across the company so they can check compensation bands and outliers. The engineer writes a query with ROW_NUMBER() because it looks simple, but the report is wrong when two executives share the same salary. One tied salary consumes an extra row, so the dashboard shows the wrong 5th place and hides the real salary band.
What goes wrong: HR opens the dashboard on Monday morning and sees a mismatch between the report and the source system. Logs show the query returned a row, but the values do not match the expected rank. Users complain that the top-compensation list is inconsistent across departments, and the fix is to switch to DENSE_RANK() and exclude NULL salaries. The bug is subtle because the query does return data; it is just the wrong data.
-- Nth Highest Salary using window functions.
-- This example shows the common interview version: Nth DISTINCT salary.
-- It also demonstrates an edge case where N is larger than the number of distinct salaries.
DROP TABLE IF EXISTS employees;
CREATE TABLE employees (
employee_id INT PRIMARY KEY,
employee_name VARCHAR(50) NOT NULL,
salary INT
);
INSERT INTO employees (employee_id, employee_name, salary) VALUES
(1, 'Ana', 100000),
(2, 'Ben', 90000),
(3, 'Cory', 90000),
(4, 'Dee', 80000),
(5, 'Eli', NULL),
(6, 'Finn', 70000);
WITH
params(n) AS (
VALUES (3), (10)
),
distinct_salaries AS (
-- DISTINCT removes duplicate salary values so ties count once.
-- NULL is excluded because it should not be ranked against real salaries.
SELECT DISTINCT salary
FROM employees
WHERE salary IS NOT NULL
),
ranked AS (
SELECT
salary,
DENSE_RANK() OVER (ORDER BY salary DESC) AS salary_rank
FROM distinct_salaries
)
SELECT
p.n AS requested_n,
r.salary AS nth_highest_salary
FROM params p
LEFT JOIN ranked r
ON r.salary_rank = p.n
ORDER BY p.n;
-- Expected results:
-- requested_n | nth_highest_salary
-- 3 | 80000
-- 10 | NULL
--
-- If you want the 2nd highest salary, change VALUES (3), (10) to VALUES (2).
-- If you want one fixed N from an application, replace params with a single bound parameter.Follow-up & Tricky Questions:
ROW_NUMBER() instead of DENSE_RANK(). That treats each row as a unique position, which is a different business rule.NULL by using a left join or a scalar wrapper, depending on how the interviewer wants the output shaped.NULL salaries be handled? Usually exclude them with WHERE salary IS NOT NULL. That keeps missing data from affecting the ranking.RANK() the same as DENSE_RANK()? No. RANK() leaves gaps after ties, while DENSE_RANK() does not. For the Nth distinct salary, DENSE_RANK() is usually the right choice.LIMIT 1 OFFSET N-1? That answers the Nth row after sorting, not the Nth distinct salary. With duplicates, it can give a different and often wrong answer.Common Mistakes:
ROW_NUMBER() when the question really asks for the distinct Nth salary. Fix: use DENSE_RANK().NULL salaries. Fix: filter them out unless the business rule says otherwise.NULL.Memory Hook: Picture a podium with tied runners: DENSE_RANK() gives the same medal to ties and never leaves an empty place number. ROW_NUMBER() is the strict attendance list; DENSE_RANK() is the real ranking board.
Cheat Sheet:
DENSE_RANK() for the Nth distinct salary.ROW_NUMBER() only when every row must be unique in order.NULL salaries unless told otherwise.rank = N after ordering salaries descending.O(n log n) sort cost on large tables.NULL.Practice Tasks:
ROW_NUMBER() and observe how the answer changes when there are duplicates.