SQL小菜之TOP N查找问题

前言

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 中三种不同的窗口排名函数,它们的主要区别在于处理相同值(并列)的方式:

  1. ROW_NUMBER()
    • 为每一行分配唯一的序号,从 1 开始递增
    • 对于相同的值,按照它们在结果集中出现的顺序分配不同的序号
    • 结果总是连续的数字,不会出现间隔
  2. RANK()
    • 为每一行分配排名,从 1 开始
    • 对于相同的值,分配相同的排名
    • 排名可能会有间隔,因为相同值后的下一个排名会跳过已用的编号
    • 例如:1, 2, 2, 4, 5...(注意跳过了 3)
  3. DENSE_RANK()
    • 为每一行分配"密集排名",从 1 开始
    • 对于相同的值,分配相同的排名
    • 排名始终是连续的,不会有间隔
    • 例如:1, 2, 2, 3, 4...(无跳过)

结语

本篇,我们简单的探讨了SQL问题中非常经典的TOP N查找问题,学习了关于该种问题的几种解法,希望对你有所帮助。

相关推荐
2301_802502331 小时前
哈工大计算机系统2025大作业——Hello的程序人生
数据库·程序人生·课程设计
Alan3165 小时前
Qt 中,设置事件过滤器(Event Filter)的方式
java·开发语言·数据库
TDengine (老段)6 小时前
TDengine 集群容错与灾备
大数据·运维·数据库·oracle·时序数据库·tdengine·涛思数据
Lao A(zhou liang)的菜园7 小时前
高效DBA的日常运维主题沙龙
运维·数据库·dba
迪迦不喝可乐7 小时前
mysql知识点
数据库·mysql
不太可爱的大白8 小时前
MySQL 事务的 ACID 四大特性及其实现原理
数据库·mysql
观测云9 小时前
HikariCP 可观测性最佳实践
数据库
文牧之9 小时前
PostgreSQL的扩展 dblink
运维·数据库·postgresql
趁你还年轻_9 小时前
Redis-旁路缓存策略详解
数据库·redis·缓存
在云上(oncloudai)10 小时前
AWS DocumentDB vs MongoDB:数据库的技术抉择
数据库·mongodb·aws