应使用ROW_NUMBER()窗口函数,因其能显式分组排序并确保每组返回唯一整行;而MAX()+GROUP BY无法保证非聚合字段与最大值同行,MySQL 8.0+开启ONLY_FULL_GROUP_BY时更会报错。

不能直接用 MAX() + GROUP BY 拿到完整行——它只返回最大值本身,其他字段是未定义的,MySQL 8.0+ 开启 ONLY_FULL_GROUP_BY 后还会报错。
为什么 MAX() + GROUP BY 不能直接取整行
执行 SELECT dept, name, MAX(salary) FROM emp GROUP BY dept 在多数数据库里会失败或返回不可靠的 name。因为 name 不在 GROUP BY 列表中,也不是聚合结果,数据库无法确定该取哪一行的 name。即使 MySQL 5.7 允许执行,name 值每次可能不同,纯属碰运气。
常见错误现象包括:
-
Expression #2 of SELECT list is not in GROUP BY clause(PostgreSQL/SQL Server/Oracle/MySQL 8.0+ 默认报错) - 返回的非聚合字段(如
user_id、created_at)与MAX()实际对应的行不一致 - 多行并列最大值时,结果不稳定或漏数据
ROW_NUMBER() 是最通用可靠的解法
窗口函数 ROW_NUMBER() 显式按组排序编号,确保每组只返回一行,语义清晰且跨库兼容(MySQL 8.0+、PostgreSQL、SQL Server、Oracle、SQLite 3.25+ 都支持)。
关键写法:ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY value_col DESC, id ASC)
-
PARTITION BY定义分组维度(如dept、sku_id) -
ORDER BY必须包含主排序列(如salary DESC)+ 至少一个能打破并列的列(如id ASC或created_at DESC),否则相同值时编号顺序不确定 - 外层筛选
WHERE rn = 1即可拿到每组“最大值所在行”的全部字段
示例:
SELECT id, dept, name, salary
FROM (
SELECT id, dept, name, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC, id ASC) AS rn
FROM emp
) t
WHERE rn = 1;
子查询和 JOIN 方案容易踩坑
老版本 MySQL(
- 用
IN或JOIN匹配(group_col, max_value)时,若该组合不唯一(比如两人同为最高薪),会返回多行——这有时是需求,有时是 bug - 相关子查询(
WHERE salary = (SELECT MAX(...) FROM ... WHERE group_col = t.group_col))在无索引时,每行触发一次子查询,10k 行就可能慢到不可接受 -
LEFT JOIN自连接方案(ON a.group = b.group AND a.value +WHERE b.group IS NULL)逻辑难读,且对 NULL 值处理脆弱 - 所有方案都依赖
value_col和group_col上有复合索引(如INDEX(dept, salary)),否则全表扫描
别用 RANK() 或 DENSE_RANK() 替代 ROW_NUMBER()
它们对相同值分配相同序号,导致 RANK() = 1 可能返回多行——如果你明确只要一行,这就是错误行为。
例如两员工工资同为 15000 并列最高,RANK() OVER (PARTITION BY dept ORDER BY salary DESC) 会给两人同时标 1,WHERE rnk = 1 就返回两条;而 ROW_NUMBER() 强制编号为 1 和 2,结果可控。
除非业务明确要求“所有并列第一都保留”,否则一律用 ROW_NUMBER(),并确保 ORDER BY 中有足够排序依据(比如加主键或时间戳)。
真正麻烦的不是语法,而是 ORDER BY 里漏掉去重字段——看起来跑通了,数据量一大或并发一高,结果就开始飘。











