Skip to content

LeetCode 176 - Second Highest Salary

Database Language: MySQL

Difficulty: ⭐⭐⭐

Problem Description

Input

Table: Employee

Column Name Type
id int
salary int

id is the primary key (column with unique values) for this table. Each row of this table contains information about the salary of an employee.

Requirement

Write a solution to find the second highest salary from the Employee table. If there is no second highest salary, return null (return None in Pandas).

The result format is in the following example.

Examples

Example 1

Input

Employee table:

id salary
1 100
2 200
3 300
Output
SecondHighestSalary
200

Example 2

Input

Employee table:

id salary
1 100
Output
SecondHighestSalary
NULL

SQL Schema

CREATE TABLE IF NOT EXISTS Employee (id INT PRIMARY KEY, salary INT);

TRUNCATE TABLE Employee;
INSERT INTO Employee (id, salary) VALUES ('1', '100');
INSERT INTO Employee (id, salary) VALUES ('2', '200');
INSERT INTO Employee (id, salary) VALUES ('3', '300');

Solutions

Solution #1 - Using LIMIT ... OFFSET

Based on the requirement of finding the second highest salary, one of the first things that may come into mind is the LIMIT ... OFFSET clause of the SELECT statement.

SELECT DISTINCT salary AS SecondHighestSalary
FROM Employee
ORDER BY salary DESC
LIMIT 1 OFFSET 1;

What this query does is to select all the distinct salary from the Employee table (SELECT DISTINCT salary FROM Employee), then sort the salary in descending order (ORDER BY salary DESC), then return just 1 row (LIMIT 1) but skipping the first row (OFFSET 1).

This query will return the second highest salary if there is more than 1 unique salary in the Employee table. But if there is only 1 unique salary in the Employee table, this query will return an empty set instead of a NULL value, which is what the question requires. To overcome this issue, a NULL salary can be included in the list of salaries using the UNION ALL statement:

SELECT DISTINCT salary FROM Employee
UNION ALL
SELECT NULL AS salary

Now that a second-highest salary will always exist, the original query above can be modified as follows:

# Final Solution 1 Query - Using LIMIT ... OFFSET
SELECT salary AS SecondHighestSalary
FROM (SELECT DISTINCT salary FROM Employee UNION ALL SELECT NULL AS salary) emp_salaries
ORDER BY salary DESC
LIMIT 1 OFFSET 1;

The fastest run-time for this query is as follows:

  • Runtime: 199ms
  • Beats: 99.76% as of July 15, 2024

Another way of writing the query is with the use of a common-table expression (CTE) wherein the list of unique salaries are created as a CTE and referenced in the main SELECT statement:

# Final Solution 1 Query - Using LIMIT ... OFFSET and CTE
WITH emp_salaries AS (
    SELECT DISTINCT salary FROM Employee
    UNION ALL
    SELECT NULL AS salary
)
SELECT salary AS SecondHighestSalary
FROM emp_salaries
ORDER BY salary DESC
LIMIT 1 OFFSET 1;

The fastest run-time for this updated query that uses a common-table expression (CTE) was the same as the one above:

  • Runtime: 203ms
  • Beats: 99.41% as of July 15, 2024

This can be explained by the query plan generated by MySQL, which was the same for both queries:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY ALL 4 100.00 Using filesort
2 DERIVED Employee ALL 3 100.00 Using temporary
3 UNION No tables used

Solution #2 - Using RANK. DENSE_RANK or ROW_NUMBER Window Function

Another way of finding the second highest salary is with the use of RANK, DENSE_RANK or even the ROW_NUMBER window functions. If the salary values being "ranked" are all unique, then the output of the RANK, DENSE_RANK and ROW_NUMBER window functions will be the same.

Just like the case in the first solution, the first step is to get all the unique salaries from the Employee table:

SELECT DISTINCT salary FROM Employee

The next step is to rank these salaries from highest to lowest:

SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) as ranking
FROM (SELECT DISTINCT salary FROM Employee) emp_salaries

Last step is to return the second highest salary:

SELECT salary AS SecondHighestSalary
FROM (SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) as ranking
      FROM (SELECT DISTINCT salary FROM Employee) emp_salaries) salary_ranking
WHERE ranking = 2;

Just like the issue in the first solution, if there is only 1 unique salary in the Employee table, this query will return an empty set instead of a NULL value, which is the required output. To overcome this issue, a NULL salary can be included in the list of salaries using the UNION ALL statement:

# Final Solution 2 Query - Using DENSE_RANK Window Function
SELECT salary AS SecondHighestSalary
FROM (SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) as ranking
      FROM (SELECT DISTINCT salary FROM Employee UNION ALL SELECT NULL AS salary) emp_salaries) salary_ranking
WHERE ranking = 2;

This query can be re-written using common-table expressions (CTEs), one for each of the sub-queries:

# Final Solution 2 Query - Using DENSE_RANK Window Function and CTE
WITH emp_salaries AS (
    SELECT DISTINCT salary FROM Employee
    UNION ALL
    SELECT NULL AS salary
),
salary_ranking AS (
    SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS ranking
    FROM emp_salaries
)
SELECT salary AS SecondHighestSalary
FROM salary_ranking
WHERE ranking = 2

As mentioned earlier, if the salaries being "ranked" are all unique, which is the case here since only the DISTINCT salary is being ranked, then the DENSE_RANK in the query can be replaced by either the RANK or ROW_NUMBER window function and the result will be the same.

Here's the query plan generated by MySQL for both of the queries above for the second solution:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY ref 8 const 1 100.00
2 DERIVED ALL 4 100.00 Using filesort
3 DERIVED Employee ALL 3 100.00 Using temporary
4 UNION No tables used
  • Runtime: 211ms
  • Beats: 96.74% as of July 15, 2024

Solution #3 - Using MAX Aggregate Function and Subquery

There is yet a third way of getting the second highest salary from the Employee table without the use of a window function such as RANK, DENSE_RANK or ROW_NUMBER and without the use of the LIMIT ... OFFSET clause of the SELECT statement.

Another way of looking at the question is, what is the highest salary that is less than the highest salary. The highest salary from the Employee table can be derived by the following query:

SELECT MAX(salary) FROM Employee

This query just needs to be plugged into another query that will get the highest salary that is less than the output of this query and that query will look as follows:

# Final Solution 3 Query - Using MAX Aggregate Function and Subquery
SELECT MAX(salary) AS SecondHighestSalary
FROM Employee
WHERE Employee.salary < (SELECT MAX(salary) FROM Employee)

If there is no salary that is less than the highest salary, then this query will return a NULL value and not an empty set. The advantage of this solution is that it does not need to use a common-table expression (CTE) and it doesn't need to "insert" a NULL value to the list of salaries to overcome the issue of having only 1 unique salary.

Here's the query plan for this query:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY Employee ALL 3 33.33 Using where
2 SUBQUERY Employee ALL 3 100.00
  • Runtime: 204ms
  • Beats: 99.25% as of July 15, 2024

Solution Runtime Comparison

Here's the comparison of the fastest runtime for each of the solutions.

Solution # Runtime Beats
1a - Using LIMIT ... OFFSET 199ms 99.76%
1b - Using LIMIT ... OFFSET and CTE 203ms 99.41%
2 - Using RANK. DENSE_RANK or ROW_NUMBER Window Function 211ms 96.74%
3 - Using MAX Aggregate Function and Subquery 204ms 99.25%

Interestingly, the one that generated the fastest runtime is the first solution. It would have been expected that the third solution would have generated the fastest runtime given that it didn't need to create a "temporary" table that includes an extra NULL salary.