最稳妥做法是用ROW_NUMBER()窗口函数:ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY value_col DESC, id ASC),它不依赖字段唯一性,能稳定处理重复最大值,且主流数据库均支持。

用 ROW_NUMBER() 提取每组最大值所在行最稳妥
直接在分组内排序后取序号为 1 的行,是目前最通用、语义最清晰的做法。它不依赖字段唯一性,也不怕重复最大值导致多选或漏选。
关键点在于 ORDER BY 要写全:先按目标列降序,再加一个能打破并列的列(比如主键或时间戳),否则相同数值时结果不稳定。
-
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY value_col DESC, id ASC)—— 推荐写法,id确保每行有确定顺序 - 别用
RANK()或DENSE_RANK(),它们对并列值分配相同序号,会导致“取第 1 名”选出多行 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 也支持
关联子查询也能做,但性能和逻辑容易翻车
原理是对外层每行判断:“当前行的 value 是否等于它所在 group 的最大 value”。看似直观,但实际有隐藏陷阱。
- 如果某组存在多个相同最大值,子查询会返回所有这些行 —— 这有时是需求,有时是 bug
- 子查询里必须写
WHERE group_col = t1.group_col,漏掉这个关联条件就变成全表最大值,结果全错 - 没有索引时,每行都触发一次子查询,数据量稍大(比如 >10k 行)就会明显变慢
- 示例写法:
SELECT t1.* FROM orders t1 WHERE t1.amount = ( SELECT MAX(t2.amount) FROM orders t2 WHERE t2.category = t1.category );
用 MAX() + JOIN 方案只适合简单场景
先聚合出各组最大值,再跟原表 JOIN 匹配。写起来短,但只适用于“最大值本身唯一”或“你接受重复匹配”的情况。
- 如果
(category, amount)不构成唯一组合,JOIN会产生笛卡尔积,一行变多行 - 无法同时拿到“最大值对应的时间”或“最大值对应的用户ID”等其他字段,除非原表有唯一标识且你愿意多连一次
- 比窗口函数多一次扫描,小表无所谓,大表建议加复合索引:
INDEX(category, amount)
不同数据库对 GROUP BY 隐式字段的处理差异很大
像 MySQL 5.7 默认允许 SELECT category, MAX(amount), user_id FROM t GROUP BY category,但 user_id 实际返回哪一行的值是未定义的——可能每次执行都不一样。
- PostgreSQL、SQL Server、Oracle 会直接报错,拒绝这种写法
- MySQL 8.0 开启
ONLY_FULL_GROUP_BY后也会报错 - 别依赖这个行为,它不是提取“最大值所在行”,只是碰巧有时看起来对
实际业务中真正麻烦的往往不是语法,而是“最大值有并列时要不要全保留”“有没有时间戳需要一并取出”“源表有没有主键可用来稳定排序”——这些细节决定了该用窗口函数的哪个变体,而不是换种写法。

















