查询”每个部门工资最高的员工”,本质是先按部门分组求出最高薪资,再回表匹配对应员工。主流实现有四类:GROUP BY+MAX 派生表关联、关联子查询、多列 IN 子查询、以及窗口函数(ROW_NUMBER/RANK/DENSE_RANK)。最稳妥且跨版本通用的是”先聚合再 JOIN”;若数据库支持窗口函数(MySQL 8.0+、SQL Server 2005+、PostgreSQL 等均支持),用 RANK()/DENSE_RANK() 一次扫描即可,代码更简洁。需要特别留意并列情况——同部门多人同薪时,ROW_NUMBER() 只保留一人,而 RANK()/DENSE_RANK() 会把并列者全部返回。
一、为什么不能直接 GROUP BY 取员工
一个常见误区是直接写 SELECT id, name, salary, dept FROM employees GROUP BY dept。据火山引擎的技术解析,这种写法会触发 MySQL 的 ERROR 1055(违反 only_full_group_by):当按 dept 分组后,MySQL 不知道该返回同部门多个员工中的哪一个 id、name 或 salary,在严格模式下直接报错。正确做法是先算出”各部门最高薪”这个聚合值,再关联回原表取完整行。
二、方法一:GROUP BY + MAX 派生表关联(最通用)
先用子查询按部门求最高薪,再与原表 JOIN 匹配。该写法不依赖窗口函数,兼容所有 MySQL 版本:
SELECT e.id, e.emp_name, e.salary, e.dept
FROM employees e
INNER JOIN (
SELECT dept, MAX(salary) AS max_salary
FROM employees
GROUP BY dept
) dept_max
ON e.dept = dept_max.dept
AND e.salary = dept_max.max_salary;
CSDN 的 LeetCode 184 题解与火山引擎均把此作为”兼容所有版本”的推荐写法。派生表 dept_max 只存”部门→最高薪”映射,体积小、易建索引,连接高效。
三、方法二:关联子查询
把外层每行的部门传进子查询,求其部门最高薪再比对:
SELECT department, employee_name, salary
FROM employees e1
WHERE salary = (
SELECT MAX(salary)
FROM employees e2
WHERE e2.department = e1.department
);
Dev.to 与 CloudInsight 都列出此法。它的语义直观,但 CloudInsight 指出:关联子查询对外层每一行都可能重跑一次内部聚合,大表上开销高于窗口函数,性能介于”聚合 JOIN”与”窗口函数”之间。
四、方法三:多列 IN 子查询
将”部门+最高薪”作为复合条件一次性匹配,写法紧凑:
SELECT DeptID, EmpName, Salary
FROM EmpDetails
WHERE (DeptID, Salary) IN (
SELECT DeptID, MAX(Salary)
FROM EmpDetails
GROUP BY DeptID
);
DevGex 的解析说明,子查询先算出每部门的最高薪组合,主查询用 IN 同时匹配部门与薪资,直接返回所有达标的员工完整信息。注意该”行值构造器”语法在部分旧引擎上支持有限,使用前需确认方言兼容性。
五、方法四:窗口函数(推荐,处理并列)
现代数据库支持窗口函数后,这是最干净的方式。用 RANK() 或 DENSE_RANK() 在部门内按薪资降序排名,再取第 1 名:
WITH ranked AS (
SELECT department, employee_name, salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rn
FROM employees
)
SELECT department, employee_name, salary
FROM ranked
WHERE rn = 1;
ROW_NUMBER、RANK、DENSE_RANK 的区别
三者都做分区内排序,但对并列的处理不同,直接决定结果是否正确:
- ROW_NUMBER():无论薪资是否相同,都给出唯一序号。同部门两人同薪时,只保留其中一人,可能漏掉并列的最高薪员工。
- RANK():相同薪资并列同名次,下一个名次”跳号”(1,1,3…)。取
rn=1会返回所有并列最高薪者,是本题的正确选择。 - DENSE_RANK():并列同名次且不跳号(1,1,2…)。取
rn=1同样返回所有并列最高薪者,区别只在后续名次编号。
Dev.to 明确建议:需处理并列时用 DENSE_RANK() 或 RANK();只取一个代表时用 ROW_NUMBER()。CloudInsight 将 2026 年的最佳实践总结为”优先 CTE + 窗口函数”,窗口函数通常只需一次表扫描。
六、四种方法对比与选型
| 方法 | 兼容性 | 并列处理 | 扫描次数 | 推荐度 |
|---|---|---|---|---|
| GROUP BY+MAX 派生表 JOIN | 全版本 | 返回全部并列 | 1~2 次 | 通用首选 |
| 关联子查询 | 全版本 | 返回全部并列 | 可能逐行 | 语义清晰时 |
| 多列 IN 子查询 | 多数现代引擎 | 返回全部并列 | 1~2 次 | 写法紧凑 |
| 窗口函数 RANK/DENSE_RANK | MySQL 8.0+/SQL Server 2005+ 等 | 返回全部并列 | 1 次 | 首选(支持时) |
若数据库支持窗口函数,CTE + RANK() 在可读性与性能上最均衡;需兼顾老旧环境时,派生表 JOIN 是稳妥替代。
七、并列情况与避坑清单
- 并列最高薪:务必用
RANK()/DENSE_RANK()而非ROW_NUMBER(),否则会丢失同薪同事。 - ERROR 1055:避免在非聚合列上直接
GROUP BY取明细,先聚合再关联。 - 只取一个代表:业务若规定”同薪取工号最小者”,用
ROW_NUMBER() OVER (... ORDER BY salary DESC, id ASC)既处理排序又确定唯一性。 - 关联部门名称:题目常需带出部门名,把员工结果与
Department表再做一次 JOIN 即可(见 CSDN 题解完整写法)。
常见问题(FAQ)
同部门两人同薪会漏人吗?
用 ROW_NUMBER() 会漏,用 RANK()/DENSE_RANK() 取第 1 名则全部返回。
哪种写法性能最好?
支持窗口函数时,CTE+RANK 通常只扫描一次,优于逐行执行的关联子查询。
MySQL 5.7 能用窗口函数吗?
不能,窗口函数需 MySQL 8.0+;5.7 请用 GROUP BY+MAX 派生表 JOIN 或关联子查询。
本文更新于 2026 年 8 月,内容综合 Dev.to、火山引擎、DevGex、CloudInsight 等公开技术资料整理。