← All cheatsheets
Data Analyst · #068 · October 7, 2026 · 3 min read

Second highest salary in SQL: the answer that survives a tie

The SQL interview classic: why ORDER BY salary DESC LIMIT 1 OFFSET 1 breaks on ties and one-row tables, the three robust answers, and the nth and per-department variants: one page.

Get the free PDF

One page, print-ready, free to share. No signup needed.

Download the PDF

LIMIT 1 OFFSET 1 fails the interview. It is the first answer everyone writes for "find the second highest salary", and it is only right when every salary is unique. The question exists to see whether you think about ties, NULLs and tables with one row. One page on why the naive answer breaks and the three answers that hold. The print-ready A4 PDF is at the bottom.

The question

  • Find the second highest salary in the employees table.
  • What it really tests: ties, NULLs, and one-row tables.
  • Ask first: the second distinct value, or the second person?

The naive answer

  • ORDER BY salary DESC LIMIT 1 OFFSET 1.
  • It works when every salary is unique.
  • And when the table has at least two rows.

Why it breaks

  • A tie at the top: it returns the top salary again.
  • One row only: it returns no row, not NULL.
  • OFFSET skips rows, not values.

Robust answers

  • DENSE_RANK() = 2: tied salaries share a rank.
  • MAX below MAX: the highest salary under the highest, NULL if there is no second one.
  • DISTINCT plus OFFSET: correct on ties, and wrapped in a scalar subquery it returns NULL when empty.
-- MAX below MAX
SELECT MAX(salary) AS second_highest
  FROM employees
 WHERE salary < (SELECT MAX(salary) FROM employees);

-- DISTINCT + OFFSET, wrapped so an empty result is NULL
SELECT (
  SELECT DISTINCT salary
    FROM employees
   ORDER BY salary DESC
   LIMIT 1 OFFSET 1
) AS second_highest;

Nth, per department

  • Rank = n: the same query works for any n.
  • PARTITION BY department_id: the second highest in each team.
  • MAX below MAX stops scaling at n = 3: one more nested subquery per step.

The answer that survives a tie

-- second highest distinct salary, NULL if none
SELECT MAX(salary) AS second_highest
  FROM (
    SELECT salary, DENSE_RANK() OVER (
      ORDER BY salary DESC NULLS LAST) AS rk
      FROM employees
  ) ranked
 WHERE rk = 2;

-- nth highest: change the 2
-- per department: PARTITION BY department_id

Rank the salary values, not the rows. MAX() around the result does two jobs: several people on rank 2 collapse to one value, and a table with a single salary returns NULL instead of zero rows.

Gotchas

  • RANK() leaves gaps: after a tie at the top there is no rank 2.
  • ROW_NUMBER() splits ties arbitrarily, so it fails like OFFSET.
  • A NULL salary sorts first in ORDER BY ... DESC on Postgres: add NULLS LAST.

The trap: same question, three tables

Salaries in the tableLIMIT 1 OFFSET 1DENSE_RANK = 2
100, 100, 90returns 100returns 90
100 (one row)returns no rowreturns NULL
NULL, 100, 90returns 100returns 90

The DENSE_RANK column is the query above, with MAX() and NULLS LAST, on PostgreSQL.

The quiz

Which one survives a tie?

A) SELECT salary FROM employees
   ORDER BY salary DESC
   LIMIT 1 OFFSET 1

B) SELECT DISTINCT salary FROM (
   SELECT salary, DENSE_RANK()
   OVER (ORDER BY salary DESC) AS r
   FROM employees) t WHERE r = 2

B. When two people tie at the top, A skips one row and returns the top salary again. DENSE_RANK gives both of them rank 1, so rank 2 is the next distinct salary.

Interview phrasing worth memorizing: do you want the second distinct salary, and what should come back when there is none? Then I rank with DENSE_RANK.

Frequently asked questions

How do you find the second highest salary in SQL?
Rank the salary values with DENSE_RANK() OVER (ORDER BY salary DESC) and keep rank 2. Tied salaries share a rank, so rank 2 is always the next distinct value. Wrapping the result in MAX() returns one value even when several people earn it, and NULL when there is no second salary.
Why is LIMIT 1 OFFSET 1 wrong for the second highest salary?
OFFSET skips rows, not values. If two employees tie for the top salary, skipping one row lands on the other one and the query returns the top salary again. On a table with a single row it returns no row at all, where most interviewers expect NULL. It is only correct when every salary is unique and there are at least two rows.
What is the difference between DENSE_RANK, RANK and ROW_NUMBER here?
DENSE_RANK gives tied salaries the same rank and never leaves a gap, so rank 2 always exists when there is a second distinct value. RANK leaves gaps: after a two-way tie at the top the next rank is 3, so filtering on rank 2 returns nothing. ROW_NUMBER numbers tied rows in an arbitrary order, so it behaves like OFFSET.
How do you get the second highest salary per department?
Add PARTITION BY department_id inside the OVER clause: DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC). The ranking restarts in each department, and filtering on rank 2 returns the second distinct salary of every team. Departments with a single distinct salary simply have no rank 2 row.

Get the free PDF

One page, print-ready, free to share. No signup needed.

Download the PDF

More cheatsheets