group by 本身不能直接取每组最新记录,因它仅分组聚合而不保留整行;正确做法是用row_number()窗口函数按partition by分组、order by时间降序编号后取rn=1,或用关联子查询匹配max时间。

GROUP BY 本身不能直接取每组最新记录
这是最常见的误解。SQL 标准中,GROUP BY 只负责分组和聚合,不保证也不提供“取某组中时间最大/ID最大那行”的能力。如果写 SELECT *, MAX(created_at) FROM orders GROUP BY user_id,多数数据库(如 MySQL 5.7+ 严格模式、PostgreSQL)会直接报错:ERROR: column "orders.id" must appear in the GROUP BY clause or be used in an aggregate function。MySQL 5.6 或低版本可能“侥幸”返回结果,但数据完全不可靠——id 和 MAX(created_at) 很可能来自不同行。
用窗口函数 ROW_NUMBER() 是最可靠方案
适用于 PostgreSQL、SQL Server、Oracle、SQLite 3.25+、MySQL 8.0+。核心思路:先按分组和时间排序编号,再过滤出每组序号为 1 的行。
示例(取每个 user_id 下 created_at 最新的订单):
SELECT id, user_id, amount, created_at
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) ranked
WHERE rn = 1;
注意点:
-
PARTITION BY user_id对应你的分组字段,可多个,如PARTITION BY user_id, status -
ORDER BY created_at DESC决定“最新”的定义;若时间相同需稳定结果,建议补上主键,如ORDER BY created_at DESC, id DESC - 如果只要“存在即可”的最新值(不要整行),用
MAX(created_at)配合GROUP BY更高效
MySQL 5.7 或旧版 SQLite 只能用关联子查询或自连接
这些引擎不支持窗口函数,得绕路。常见写法是“找每组最大时间,再回连原表匹配整行”:
SELECT o1.* FROM orders o1 WHERE o1.created_at = ( SELECT MAX(o2.created_at) FROM orders o2 WHERE o2.user_id = o1.user_id );
但要注意:
- 如果同一
user_id有多条记录时间相同,会返回多行——这不是 bug,是逻辑必然 - 性能依赖
(user_id, created_at)联合索引,否则全表扫描很慢 - 某些场景下(如需排除已删除标记),自连接比子查询更易控制条件,但写法更冗长
用 DISTINCT ON(PostgreSQL 专属快捷写法)
PostgreSQL 提供了语法糖,语义清晰且通常比窗口函数稍快:
SELECT DISTINCT ON (user_id)
id, user_id, amount, created_at
FROM orders
ORDER BY user_id, created_at DESC;
关键约束:
-
DISTINCT ON字段必须是ORDER BY的前缀,否则报错:SELECT DISTINCT ON expressions must match initial ORDER BY expressions - 只适用于 PostgreSQL,其他数据库不识别该语法
- 如果
created_at相同,它取排序后第一行,但无法指定“再按 id 降序”,需在ORDER BY中显式补全,如ORDER BY user_id, created_at DESC, id DESC
实际选哪种,取决于你用的数据库版本和是否允许重复时间。窗口函数通用性最强,但旧环境只能妥协;DISTINCT ON 看起来简洁,却把迁移成本锁死了。真正上线前,务必在真实数据量下测执行计划——尤其是 created_at 为空或存在大量 NULL 时,ORDER BY ... DESC 的行为可能和预期不一致。











