前言
SQL的编写是后端面试中非常常见,其中TOP N查找问题也是高频出现的问题,今天我们来看两道SQL TOPN问题。
问题
我们有一张雇员表Employee:
sql
CREATE TABLE `Employee` (
`id` int DEFAULT NULL,
`salary` int DEFAULT NULL,
`department` varchar(16) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
Question1:
sql
查询并返回 Employee 表中第二高的 不同 薪水 。如果不存在第二高的薪水,
查询应该返回 null。
查询结果如下例所示。
示例1:
输入:
Employee 表:
+----+--------+
| id | salary |
+----+--------+
| 1 | 100 |
| 2 | 200 |
| 3 | 300 |
+----+--------+
输出:
+---------------------+
| SecondHighestSalary |
+---------------------+
| 200 |
+---------------------+
示例2:
输入:
Employee 表:
+----+--------+
| id | salary |
+----+--------+
| 1 | 100 |
+----+--------+
输出:
+---------------------+
| SecondHighestSalary |
+---------------------+
| null |
+---------------------+
这就是最典型一道TOP N查找问题,本题要求我们查找第二高薪水的记录,我们来将问题进行拆解:
如果要查找第二高的记录,那是不是找到第一高的记录,然后在小于该记录的结果集中,找到最大的那一个,就是我们想要的结果。
OK,思路明确,开始写SQL,先找到第一高那条记录:
sql
select max(distinct(salary)) from Employee
因为题干中没有强调记录不重复,因此需要进行去重,接下来第二步,从比它小的结果集中找到最大的那个,就是我们想要的答案:
sql
select max(distinct(salary)) from Employee
where salary < (select max(distinct(salary)) from Employee)
我们同时要考虑一些异常的情况,例如表中如果仅有一条记录,那么结果需要输出null:
sql
select ifNull((max(distinct(salary))), null) as 'SecondHighestSalary' from Employee
where salary < (select max(distinct(salary)) from Employee);
针对这道题,这个SQL可以解决问题,那么题目换一下,如果不是找第二高的记录呢?如果找第三高,怎么办?
上面的SQL就不能满足我们的需求了,换一个思路,如果要找第N高的记录,我们是不是可以对结果集进行排序,然后把前面的记录丢弃,剩下的结果集取第一个,就是我们想要的结果了?诶?这不就是limit offset么?
sql
select ifNull(
(select distinct(salary) from Employee order by salary desc limit 1,1), null)
as SecondHighestSalary;
使用limit offset的方式,可以解决大部分简单的TOP N问题了,那么如果修改一下题目,再加入部门的维度,希望找到每个部门中,薪水第二高的记录,上面的解法,似乎就没有办法做到了,那么是否还有其他的解决方法呢?
MySQL在8.0版本中加入了窗口函数
,专门为了解决排行问题,来看一下dense_rank()的使用语法:
sql
DENSE_RANK() OVER (
[PARTITION BY column1, column2, ...]
ORDER BY column1 [ASC|DESC], column2 [ASC|DESC]
)
看起来有点乱哈,没关系,我们来看个示例来帮你理解dense_rank()。
上面的问题,如果用dense_rank()解决:
sql
select (
select distinct salary from (
select dense_rank() over ( order by salary desc) as rn, u.* from Employee u
) t where t.rn = 2) as SecondHighestSalary;
单看上面的SQL你可能还是会一脸懵逼,没关系,让我们来拆解一下SQL,先看最中间的核心子查询:
sql
select dense_rank() over ( order by salary desc) as rn, u.* from Employee u;
+----+------+--------+------------+
| rn | id | salary | department |
+----+------+--------+------------+
| 1 | 4 | 400 | 二部 |
| 2 | 3 | 300 | 二部 |
| 3 | 2 | 200 | 一部 |
| 3 | 2 | 200 | 一部 |
| 4 | 1 | 100 | 一部 |
+----+------+--------+------------+
可以发现dense_rank()对结果集进行了排序处理,并输出了根据目标字段排序的记录的排名序号,那么再回头看上面的SQL就非常好理解了,我们需要找到排名第二的记录,通过where限定,即可达到预期结果。
再回到上上个问题,如果我们希望查找每个部门中,薪水第二高的记录,怎么做?
聪明的你肯定想到,对部门字段进行group by,再找到第二高的记录,幸运的是,dense_rank()提供了根据字段进行分组的功能,就是PARTITION BY
参数,那么来看一下SQL该怎么写:
sql
SELECT t.department, t.salary FROM (
SELECT
salary,
department,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) as rn
FROM Employee
) t
WHERE rn = 2;
DENSE_RANK()、ROW_NUMBER() 和 RANK()对比
最后,我们来看一下MySQL中关于处理排名相关的几个函数,DENSE_RANK、ROW_NUMBER 和 RANK 是 SQL 中三种不同的窗口排名函数,它们的主要区别在于处理相同值(并列)的方式:
- ROW_NUMBER()
- 为每一行分配唯一的序号,从 1 开始递增
- 对于相同的值,按照它们在结果集中出现的顺序分配不同的序号
- 结果总是连续的数字,不会出现间隔
- RANK()
- 为每一行分配排名,从 1 开始
- 对于相同的值,分配相同的排名
- 排名可能会有间隔,因为相同值后的下一个排名会跳过已用的编号
- 例如:1, 2, 2, 4, 5...(注意跳过了 3)
- DENSE_RANK()
- 为每一行分配"密集排名",从 1 开始
- 对于相同的值,分配相同的排名
- 排名始终是连续的,不会有间隔
- 例如:1, 2, 2, 3, 4...(无跳过)
结语
本篇,我们简单的探讨了SQL问题中非常经典的TOP N查找问题,学习了关于该种问题的几种解法,希望对你有所帮助。