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 + OFFSETO(n log n) — DISTINCT + ORDER BY 排序主导O(n) — 去重中间结果
DENSE_RANKO(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 函数中,LIMITOFFSET 子句不能直接使用表达式(如 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 也兜回。

相关链接