184. 部门工资最高的员工(Department Highest Salary)

难度:中等 | 考点:窗口函数 PARTITION BY、DENSE_RANK、JOIN、子查询 + 聚合

题目

Employee 表(Id, Name, Salary, DepartmentId)和 Department 表(Id, Name)。查询每个部门工资最高的员工。如果有并列最高,都显示。

示例

 
Employee 表:
 
| id | name  | salary | departmentId |
 
| 1  | Joe   | 70000  | 1            |
 
| 2  | Jim   | 90000  | 1            |
 
| 3  | Henry | 80000  | 2            |
 
| 4  | Sam   | 60000  | 2            |
 
| 5  | Max   | 90000  | 1            |
 
Department 表:
 
| id | name  |
 
| 1  | IT    |
 
| 2  | Sales |
 
输出:
 
| Department | Employee | Salary |
 
| IT         | Jim      | 90000  |
 
| Sales      | Henry    | 80000  |
 
| IT         | Max      | 90000  |
 

思路

先讲个故事:各部门的 MVP

公司要评选每个部门的 MVP(最有价值员工)。做法是:

  1. 按部门分组(PARTITION BY)——IT 部门和 Sales 部门各自独立评选

  2. 按工资排名(ORDER BY Salary DESC)——工资最高的排第 1

  3. 取第 1 名(WHERE rnk = 1)——只保留每个部门的第一名

如果有两个人并列最高(比如 Jim 和 Max 都是 90000),两人都要显示——这就是 DENSE_RANK 的作用。

引导式推导:两种解法

解法一:窗口函数 DENSE_RANK + PARTITION BY(推荐)


graph LR

    A["PARTITION BY<br/>按部门分组"] --> B["ORDER BY Salary DESC<br/>工资降序排名"]

    B --> C["DENSE_RANK<br/>密集排名"]

    C --> D["WHERE rnk = 1<br/>取每组第一名"]

    D --> E["JOIN Department<br/>获取部门名"]

解法二:子查询 + 聚合(不用窗口函数)


graph LR

    A["GROUP BY DepartmentId<br/>按部门分组"] --> B["MAX(Salary)<br/>每个部门最高工资"]

    B --> C["IN 子查询<br/>匹配最高工资的员工"]

    C --> D["JOIN Department<br/>获取部门名"]

PARTITION BY vs GROUP BY

PARTITION BYGROUP BY
作用窗口函数分区,不压缩行数聚合,压缩行数
结果行数与原表相同分组数(通常更少)
使用场景排名、累计、移动平均等求和、计数、最大值等聚合

SQL 代码

方法一:窗口函数 DENSE_RANK + PARTITION BY(推荐)

 
SELECT Department, Employee, Salary
 
FROM (
 
    SELECT
 
        d.Name AS Department,
 
        e.Name AS Employee,
 
        e.Salary,
 
        DENSE_RANK() OVER (
 
            PARTITION BY e.DepartmentId
 
            ORDER BY e.Salary DESC
 
        ) AS rnk
 
    FROM Employee e
 
    JOIN Department d
 
        ON e.DepartmentId = d.Id
 
) t
 
WHERE rnk = 1;
 

方法二:子查询 + 聚合(不用窗口函数)

 
SELECT
 
    d.Name AS Department,
 
    e.Name AS Employee,
 
    e.Salary
 
FROM Employee e
 
JOIN Department d
 
    ON e.DepartmentId = d.Id
 
WHERE (e.DepartmentId, e.Salary) IN (
 
    SELECT DepartmentId, MAX(Salary)
 
    FROM Employee
 
    GROUP BY DepartmentId
 
)
 
ORDER BY d.Name;
 

复杂度

方法时间复杂度空间复杂度
DENSE_RANK + PARTITION BYO(n log n) — 窗口函数内部排序O(n) — 临时存储排名结果
子查询 + MAX + INO(n) — GROUP BY 聚合 + IN 匹配O(k) — k 为部门数

子查询+聚合版时间复杂度更优(O(n) vs O(n log n)),但窗口函数版可读性更好、扩展性更强(如改查前三高只需改 rnk <= 3)。


实战考量

频率分析

出现在:SQL 高频经典题,约 45% 的同类题会考。关键在于你PARTITION BY 的理解,以及能不能同时写窗口函数版和子查询版。

延伸思考

Q:为什么用 DENSE_RANK 而不是 RANK?

A:对于”取第一名”这个场景,DENSE_RANK 和 RANK 结果一样(并列第一都是 rnk=1)。但 DENSE_RANK 语义更清晰:排名连续不跳号。

Q:为什么用 PARTITION BY?

A:PARTITION BY 将数据按部门分成独立的分区,每个分区内独立排名。不用 PARTITION BY,DENSE_RANK 会全局排名,只有全公司第一名会被选出来。

Q:如果不用窗口函数怎么做?

A:用子查询 + 聚合(见方法二)。核心是先 GROUP BY 找出每个部门的最高工资,再用 IN 匹配。

Q:如果有员工不属于任何部门(DepartmentId 为 NULL)?

A:用 INNER JOIN 会被过滤掉。如果需要保留,改用 LEFT JOIN,但 NULL 部门的”最高工资”需要额外处理。

Q:如果要查每个部门工资前三高的所有员工?

A:将 WHERE rnk = 1 改为 WHERE rnk <= 3。这是 LeetCode 185 题。

易错点

  • 忘记 PARTITION BY,变成全局排名

  • 用 ROW_NUMBER 而不是 DENSE_RANK(并列最高会只取一个)

  • JOIN 条件写错(如用错列名)


生活类比

部门最高工资 → 各部门的 MVP 评选

按部门分组(PARTITION BY),组内按工资排名(ORDER BY DESC),取每组第一名(WHERE rnk = 1)。如果有并列最高,两人都要显示。

一句话总结:分组 → 排名 → 取第一名。


相关题目

题目关系
178分数排名DENSE_RANK 基础(无 PARTITION BY)
180连续出现的数字窗口函数 LEAD/LAG
177第N高的薪水全局排名取第 N 名

→ 返回题单:LeetCode学习路线图 > 十六、SQL 高频

速记卡(面试闪卡)

Q1:一句话讲清「184. 部门工资最高的员工(Department Highest Salary)」到底是什么?

A:Employee 表(Id, Name, Salary, DepartmentId)和 Department 表,查询每个部门工资最高的员工,若有并列最高则都显示。SQL 高频经典题。

Q2:题目与部门 MVP —— 怎么理解?

A:像公司评各部门 MVP——按部门分组(PARTITION BY)、组内按工资降序排名(ORDER BY DESC)、取每组第一名(WHERE rnk=1)。如果两人并列最高(如都是 90000),两人都要显示,这正是 DENSE_RANK(密集排名)的作用。

Q3:解法一窗口函数 —— 怎么理解?

A:DENSE_RANK() OVER (PARTITION BY DepartmentId ORDER BY Salary DESC) AS rnk,外层 WHERE rnk = 1 再 JOIN Department 取部门名。可读性好、扩展强——改查前三高只需 rnk <= 3(185 题)。

Q4:解法二子查询 —— 怎么理解?

A:先 GROUP BY DepartmentId + MAX(Salary) 找各部门最高工资,再用 (DepartmentId, Salary) IN (...) 匹配员工。时间 O(n) 优于窗口函数的 O(n log n),但可读性差些。

Q5:易错与延伸 —— 怎么理解?

A:忘了 PARTITION BY 会变成全公司排名只选一个;用 ROW_NUMBER 会因并列只取一个(必须用 DENSE_RANK);若员工 DepartmentId 为 NULL,INNER JOIN 会被过滤,要保留得用 LEFT JOIN。

Q6:核心速记主线有哪些?

A:题目、分组排名取第一、DENSE_RANK、子查询 + MAX + IN、PARTITION 易错、前三高变体。

口诀

A:部门最高工资查,分区排名取第一;

并列用 DENSE_RANK,子查询法 MAX 配 IN。

相关链接