177. 第N高的薪水(Nth Highest Salary)
难度:中等 | 考点:CREATE FUNCTION、变量、LIMIT OFFSET、窗口函数 DENSE_RANK
题目
Employee 表(Id, Salary),编写一个 SQL 函数 getNthHighestSalary(N INT),返回第 N 高的薪水。如果不存在,返回 NULL。
示例
输入:n = 2
输出:
| getNthHighestSalary(2) |
| 200 |
思路
先讲个故事:通用颁奖模板
上次颁奖典礼(176题)是请第二名上台。这次老板说:“我不想只请第二名,我要一个通用模板——第 N 名都行。”
你发现模板和上次几乎一样,只是 OFFSET 从 1 变成了 N-1。但 MySQL 函数里有个坑:LIMIT 不能直接写表达式 N-1,必须先用变量存起来。
通用模板 = 去重 → 降序 → 跳过 N-1 行 → 取 1 行 → IFNULL 兜底
引导式推导:LIMIT+OFFSET vs 窗口函数
方法一:LIMIT + OFFSET
graph LR A["DECLARE M = N-1"] --> B["DISTINCT 去重"] B --> C["ORDER BY DESC"] C --> D["LIMIT 1 OFFSET M"] D --> E["IFNULL 兜底"]
方法二:DENSE_RANK 窗口函数(推荐)
graph LR A["DENSE_RANK() OVER<br/>ORDER BY Salary DESC"] --> B["WHERE rnk = N"] B --> C["DISTINCT 去重"] C --> D["IFNULL 兜底"]
DENSE_RANK vs RANK vs ROW_NUMBER
| 函数 | 行为 | 示例(薪水 100, 100, 90, 80) |
|---|---|---|
| DENSE_RANK | 连续不跳号 | 1, 1, 2, 3 |
| RANK | 跳号 | 1, 1, 3, 4 |
| ROW_NUMBER | 唯一编号 | 1, 2, 3, 4 |
用 RANK 找第 2 高会出错(因为并列第 1 后直接跳到第 3)。DENSE_RANK 最合适。
SQL 代码
方法一:LIMIT + OFFSET(函数写法)
CREATE FUNCTION getNthHighestSalary(N INT) RETURNS INT
BEGIN
DECLARE M INT;
SET M = N - 1;
RETURN (
SELECT IFNULL(
(SELECT DISTINCT Salary
FROM Employee
ORDER BY Salary DESC
LIMIT 1 OFFSET M),
NULL
)
);
END
方法二:窗口函数 DENSE_RANK(推荐)
CREATE FUNCTION getNthHighestSalary(N INT) RETURNS INT
BEGIN
RETURN (
SELECT DISTINCT Salary
FROM (
SELECT Salary,
DENSE_RANK() OVER (
ORDER BY Salary DESC
) AS rnk
FROM Employee
) t
WHERE rnk = N
);
END
复杂度
| 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| LIMIT + OFFSET | O(n log n) — DISTINCT + ORDER BY 排序主导 | O(n) — 去重中间结果 |
| DENSE_RANK | O(n log n) — 窗口函数内部需要排序 | O(n) — 窗口计算临时存储 |
实战考量
频率分析
出现在:176 题的通用版,约 35% 的同类题会考。关键在于能不能把特例推广到通用,以及对窗口函数的掌握。
延伸思考
Q:为什么 SET M = N - 1?
A:OFFSET 从 0 开始计数。第 1 高跳过 0 行,第 2 高跳过 1 行,第 N 高跳过 N-1 行。
Q:为什么需要 DECLARE 变量?
A:MySQL 函数中,LIMIT 和 OFFSET 子句不能直接使用表达式(如 LIMIT 1 OFFSET N-1),必须先将计算结果存入变量。
Q:DENSE_RANK vs RANK vs ROW_NUMBER 的区别?
A:见上方对比表。DENSE_RANK 连续不跳号,RANK 并列会跳号,ROW_NUMBER 唯一编号。找”第 N 高”用 DENSE_RANK。
Q:为什么要用 DISTINCT?
A:第 N 高是按去重后的薪水排的。如果两个员工都是 100 万,他们都算第 1 高。
Q:如果 N 为负数或 0 怎么办?
A:可以在函数开头加判断:IF N <= 0 THEN RETURN NULL; END IF;
易错点
-
忘记 OFFSET 从 0 开始,写成 OFFSET N(应该是 N-1)
-
用 RANK 而不是 DENSE_RANK(并列会跳号)
-
忘记 DISTINCT
生活类比
第 N 高 → 通用颁奖模板
上次请第二名,这次要一个通用模板:去重、排队、跳过前 N-1 名、请第 N 名上台。MySQL 有个坑——LIMIT 不接受表达式,必须先用变量存好跳过几行。
一句话总结:去重、降序、跳过 N-1 行、取 1 行。
相关题目
| 题目 | 关系 |
|---|---|
| 176第二高的薪水 | N=2 的特例 |
| 178分数排名 | DENSE_RANK 的经典应用 |
| 184部门工资最高的员工 | PARTITION BY + DENSE_RANK |
→ 返回题单:LeetCode学习路线图 > 十六、SQL 高频
速记卡(面试闪卡)
Q1:一句话讲清「177. 第N高的薪水(Nth Highest Salary)」到底是什么?
A:第 N 高薪水问题:写一个 SQL 函数,返回某张员工表里排名第 N 高的薪水,没有就返回 NULL。
Q2:通用颁奖模板怎么理解 —— 怎么理解?
A:像颁奖典礼请第 N 名上台:先给薪水去重排队,跳过前 N-1 名,请第 N 名出场,没人的话 IFNULL 兜底发”空”。LIMIT + OFFSET 法就是去重→降序→跳过 N-1 行→取 1 行。
Q3:为什么要用变量存 N-1 —— 怎么理解?
A:像菜单写不了”跳到第 N-1 名”这种算式:MySQL 里 LIMIT / OFFSET 不能直接吃表达式 N-1,必须先用 DECLARE 把 M=N-1 存好,再 LIMIT 1 OFFSET M。这是函数里最容易踩的坑。
Q4:DENSE_RANK 窗口函数怎么理解 —— 怎么理解?
A:像给并列的人发连号胸牌:两人同薪都算第 1,下一位是第 2(不断号)。DENSE_RANK() 窗口函数(Window Function)按薪水降序排名,WHERE rnk=N 直接捞出第 N 高,比 LIMIT 法更稳。
Q5:DENSE_RANK vs RANK vs ROW_NUMBER —— 怎么理解?
A:像三种数数法:DENSE_RANK 并列不断号(1,1,2,3),RANK 并列跳号(1,1,3,4)会漏掉真正第 2,ROW_NUMBER 强行给每人独号(1,2,3,4)。找”第 N 高”要用不断号的 DENSE_RANK。
Q6:核心速记主线有哪些?
-
去重排队:DISTINCT 按薪水排,并列只算一次
-
跳过前 N-1:OFFSET 从 0 计数,第 N 高跳过 N-1 行
-
变量存算式:LIMIT 不吃表达式,先 DECLARE M=N-1
-
窗口更稳:DENSE_RANK 不断号,WHERE rnk=N 直取
口诀
A:颁奖请第N,去重排个队;
跳过前N-1,变量先把坑填。
DENSE_RANK 连号发,并列不跳位;
窗口函数稳,NULL 也兜回。