GROUP BY 后直接用 MAX() 只返回每组最大值,不保留对应行其他字段;若需完整记录,MySQL 8.0+ 推荐用 ROW_NUMBER() 窗口函数,老版本可用子查询或 LEFT JOIN。

GROUP BY 后直接用 MAX() 是最常用但容易误解的写法
MAX() 和 GROUP BY 一起用,确实能拿到每组的最大值,但它只返回聚合结果,不保留对应那行的其他字段。比如你查 SELECT dept, MAX(salary) FROM emp GROUP BY dept,能知道每个部门最高工资是多少,但不知道是谁拿的、入职时间、职位——这些信息全丢了。
常见错误现象:SELECT dept, name, MAX(salary) FROM emp GROUP BY dept 在 MySQL 5.7+ 默认会报错(sql_mode=only_full_group_by 开启时),因为 name 不在 GROUP BY 里,也不是聚合函数结果,数据库没法确定该取哪一行的 name。
- 如果你只要最大值本身,
MAX()+GROUP BY完全够用 - 如果还要整行数据(比如“工资最高的员工完整信息”),就不能只靠
MAX(),得换思路 - MySQL 8.0+、PostgreSQL、SQL Server 支持窗口函数,是更稳妥的选择
想查“每组中 salary 最大的那条完整记录”,用窗口函数 ROW_NUMBER()
核心思路:先按分组排序,给每组内的行编号,再筛出序号为 1 的行。比子查询或 JOIN 更直观,也避免了 MAX() + 关联查询时可能的多行匹配问题(比如两个员工工资并列最高)。
实操建议:
- 用
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC)给每组按工资降序编号 - 外层套一层查询,过滤
rn = 1即可拿到每组“第一个最高工资员工” - 如果要包含并列情况(即所有最高工资者都返回),改用
RANK()或DENSE_RANK()
示例(MySQL 8.0+):
SELECT dept, name, salary FROM (SELECT dept, name, salary, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn FROM emp ) t WHERE rn = 1;
兼容老版本 MySQL(5.7 及以下)只能靠相关子查询或 LEFT JOIN
窗口函数不可用时,最容易想到的是子查询:WHERE salary = (SELECT MAX(salary) FROM emp e2 WHERE e2.dept = e1.dept)。但要注意:它会在每行都执行一次子查询,数据量大时性能明显下降;而且当存在并列最大值时,它会正确返回所有并列者——这点反而比 ROW_NUMBER() 更符合直觉。
另一个方案是自连接:LEFT JOIN emp e2 ON e1.dept = e2.dept AND e1.salary,逻辑是“找不到同部门更高工资的人”,就说明当前行是最高。这个写法在某些场景下执行计划更优,但可读性差,且容易漏掉 NULL 值处理。
- 子查询方式简洁易懂,适合中小表;注意加
dept和salary联合索引提升性能 - JOIN 方式对大表可能更快,但必须确保
salary字段非 NULL,否则e1.salary 会让 NULL 行被过滤掉 - 两种方式在有重复最大值时行为一致,都能返回全部并列记录
别忽略 NULL 和数据类型隐式转换带来的偏差
MAX() 对 NULL 值默认忽略,这通常没问题;但如果你的 salary 字段是字符串类型(比如存了“$5000”),MAX() 就会按字典序比较,导致“$900”>“$10000”。这种错误不会报错,但结果完全错乱。
- 检查字段类型:用
DESCRIBE emp或SHOW COLUMNS FROM emp确认salary是数值型(如DECIMAL、INT) - 如果数据里混了单位或符号,先清洗再计算,不要依赖
CAST()或REPLACE()在查询里硬转——影响索引使用和性能 - ORDER BY 中用
NULLS LAST(PostgreSQL)或IS NULL排序(MySQL)来控制 NULL 的位置,尤其当你用RANK()时,NULL 可能被排第一
真正麻烦的不是语法怎么写,而是搞清你要的到底是“最大值”,还是“最大值所在行”,或是“所有并列最大值的行”——这三个需求对应三种完全不同的写法,选错一个,结果就偏了。