最可靠方式是用 row_number() 窗口函数:按 user_id 分组、created_at 和 id 降序排序,取 rn=1 的行;需建 (user_id, created_at, id) 联合索引以保性能。

用窗口函数 ROW_NUMBER() 最可靠
绝大多数现代数据库(PostgreSQL、SQL Server、Oracle、MySQL 8.0+)都支持 ROW_NUMBER(),这是解决“每组最新一条”最清晰、最不易出错的方式。关键在于按分组字段 PARTITION BY,再按时间/序号字段 ORDER BY ... DESC 排序,取 rn = 1 的行。
常见错误是把 ORDER BY 写成升序(ASC),结果拿到的是最早记录;或者漏写 PARTITION BY,导致全表只排一次序。
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY created_at DESC, id DESC
) AS rn
FROM orders
) t WHERE rn = 1;
-
created_at DESC确保最新时间优先;加id DESC是为时间相同时有确定性排序(避免随机取) - 别名
t必须存在,否则外部WHERE无法引用rn - 如果表很大,确保
(user_id, created_at, id)有联合索引,否则性能会明显下降
MySQL 5.7 或不支持窗口函数时用自连接
老版本 MySQL 或 SQLite 等不支持 ROW_NUMBER() 的场景,得靠关联子查询或自连接。核心思路是:对每条记录,检查同组内是否存在“更新”的记录;若不存在,它就是最新的。
容易踩的坑是没加 OR 条件处理时间相同的情况,导致多条记录并列最新却被漏掉;还有子查询没加 WHERE 过滤组条件,变成全表扫描。
SELECT o1.* FROM orders o1
LEFT JOIN orders o2
ON o1.user_id = o2.user_id
AND (o2.created_at > o1.created_at
OR (o2.created_at = o1.created_at AND o2.id > o1.id))
WHERE o2.id IS NULL;
- 两个判断条件用括号包住,确保逻辑优先级正确
-
o2.id IS NULL表示没找到比它更新的同组记录——这才是“最新”的本质定义 - 这个写法在大数据量下性能较差,务必给
user_id和created_at建复合索引
用 GROUP BY + 聚合函数会丢字段
很多人第一反应是 SELECT user_id, MAX(created_at) FROM orders GROUP BY user_id,但这只能拿到时间,拿不到整行数据(比如订单金额、状态等)。强行在 SELECT 里加其他字段,MySQL 5.7 默认报错,8.0+ 则行为不可靠(可能返回任意一行)。
除非你只要分组键和聚合值,否则这条路走不通。硬要这么写,结果不是错就是误导。
-
MAX(created_at)正确,但amount字段不能直接跟在后面——它不属于GROUP BY键,也不参与聚合 - 某些旧版 MySQL 开启了
sql_mode=ONLY_FULL_GROUP_BY会直接拒绝执行 - 即使执行成功,
amount可能来自同一组里任意一条记录,完全不可控
PostgreSQL 中 DISTINCT ON 更简洁
PostgreSQL 提供了语法糖 DISTINCT ON,语义更贴近“每组取第一条”,写起来比窗口函数少几行,但仅限 PG 使用。
注意它必须配合 ORDER BY,且 DISTINCT ON 的字段必须出现在 ORDER BY 最左侧,否则报错。
SELECT DISTINCT ON (user_id) user_id, created_at, amount, status FROM orders ORDER BY user_id, created_at DESC, id DESC;
-
DISTINCT ON (user_id)表示按user_id分组,“每组只留排序后第一条” -
ORDER BY必须以user_id开头,否则语法错误 - 性能上和窗口函数接近,同样依赖
(user_id, created_at, id)索引
实际选哪种方案,取决于你用的数据库版本和是否允许引入新特性。窗口函数通用性最强,但老系统只能退回到自连接;PG 用户可以省点力气,但别指望迁到 MySQL 时还能用 DISTINCT ON。时间字段为空、时区不一致、并发写入导致时间重复——这些细节才是真正卡住人的地方。










