直接用group by拿不到最新记录,因为group by仅聚合不筛选,max(created_at)只返回时间值,无法绑定对应行的id、status等字段;mysql 5.7+默认启用only_full_group_by会报错,而row_number()窗口函数通过partition by user_id order by created_at desc, id desc编号后取rn=1,可稳定获取每组最新完整记录。

为什么直接用 GROUP BY 拿不到最新记录
因为 GROUP BY 本身不保证组内哪一行被选中,MAX(created_at) 可以拿到时间最大值,但无法同时拉出对应那行的 id、status 等字段——MySQL 5.7 默认开启 ONLY_FULL_GROUP_BY,直接 SELECT id, MAX(created_at) 会报错:Expression #1 of SELECT list is not in GROUP BY clause。
常见错误写法是先 GROUP BY 再用子查询关联,但容易漏掉并列最新(同秒级时间)或性能爆炸(没索引时全表嵌套扫描)。
用窗口函数 ROW_NUMBER() 最稳(MySQL 8.0+ / PostgreSQL / SQL Server)
核心思路:给每组按时间倒序编号,取序号为 1 的行。它天然处理并列情况(按 id 或其他字段二次排序可控制“谁算最新”)。
- 必须写
ORDER BY created_at DESC, id DESC—— 否则同时间记录顺序不确定,ROW_NUMBER()结果不可靠 - 别用
RANK()或DENSE_RANK(),它们对相同时间会分配相同序号,导致返回多条 - 示例:
SELECT id, user_id, status, created_at
FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY created_at DESC, id DESC
) AS rn
FROM orders
) t
WHERE rn = 1;
兼容老版本 MySQL(5.7 及以下)的 LEFT JOIN 自连接方案
本质是“找不出比自己更新的同组记录”的行,逻辑清晰但易写错连接条件。
- 连接条件必须包含
t1.user_id = t2.user_id和t1.created_at (注意是 <li> <code>WHERE t2.id IS NULL才能筛出“没有更晚记录”的行;漏掉这句就变成普通连接 - 如果时间字段允许 NULL,要额外加
AND t1.created_at IS NOT NULL,否则 NULL 会被当成“最新” - 性能依赖
(user_id, created_at)联合索引,否则JOIN会很慢
WHERE IN + 子查询的坑与适用场景
看起来简洁,但仅适合“每组唯一时间戳”且数据量不大时用,否则可能重复或超时。
- 子查询必须返回
(user_id, created_at)二元组,不能只返回MAX(created_at)—— 否则无法关联到具体行 - 写成
WHERE (user_id, created_at) IN (SELECT user_id, MAX(created_at) ...)是可行的,但 MySQL 对多列IN的优化一般,上万行就明显变慢 - 如果同一
user_id有多条记录时间完全相同,这个方案会全部返回,不是“一条” - PostgreSQL 支持
DISTINCT ON,比这个更直接,但 MySQL 没这语法
真正复杂的地方不在写法本身,而在你是否清楚“最新”到底指什么:是最后插入?最后修改?还是业务上定义的某种状态变更时间?不同定义可能需要不同的排序字段和去重逻辑。










