最可靠方法是用窗口函数ROW_NUMBER()配合PARTITION BY和ORDER BY排序后取rn=1的行;GROUP BY本身无法直接取最新整行,因非聚合字段未在GROUP BY中会导致结果不可靠,且MAX()仅返回值不绑定整行。

用窗口函数 ROW_NUMBER() 配合 OVER 是最可靠的方法
直接在 GROUP BY 里“取最新一条”是行不通的——GROUP BY 本身不保留行序,也没有内置的“取最大时间那行”的语义。真正可行的是先排序编号,再过滤。核心思路:按分组字段和时间字段(如 created_at)排序,给每组内记录打上序号,取序号为 1 的行。
常见错误是试图用 MAX(created_at) 配合 GROUP BY 再连表回查,但一旦有多个记录共享同一最大时间,就会漏数据或重复;而 ROW_NUMBER() 能稳定选出唯一一行(即使时间相同,也能靠额外排序规则破歧义)。
示例(PostgreSQL / MySQL 8.0+ / SQL Server):
SELECT id, user_id, status, created_at
FROM (
SELECT id, user_id, status, created_at,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY created_at DESC, id DESC
) AS rn
FROM orders
) t
WHERE rn = 1;
-
PARTITION BY user_id对应你要的“每组”逻辑(比如每个用户一组) -
ORDER BY created_at DESC, id DESC确保时间最新优先;加id DESC是为了时间相同时仍能确定性选中一条(避免因无主键/无索引导致结果不稳定) - 别用
RANK()或DENSE_RANK()——它们对并列值分配相同序号,可能返回多行
MySQL 5.7 或 SQLite 等不支持窗口函数时,用相关子查询兜底
老版本数据库没 ROW_NUMBER(),就得靠子查询找每组内的最大时间,再关联原表。性能较差,且必须确保时间字段+分组字段组合能唯一标识目标行(否则仍可能取到多条)。
示例(MySQL 5.7):
SELECT o1.id, o1.user_id, o1.status, o1.created_at FROM orders o1 WHERE o1.created_at = ( SELECT MAX(o2.created_at) FROM orders o2 WHERE o2.user_id = o1.user_id );
- 如果存在两条记录
user_id=100且created_at完全相同,这个查询会返回两条——不是“每组一条”,而是“每组所有最新时间的行” - 要强行只取一条,得再加一层限制,比如
AND o1.id = (SELECT MAX(o2.id) ...),但嵌套变深、可读性下降 - 务必确认
(user_id, created_at)有联合索引,否则子查询会全表扫描,大表直接卡死
GROUP BY 加聚合函数(如 MAX())只能取字段值,不能取整行
这是最容易误解的点。写 SELECT user_id, MAX(created_at), status FROM orders GROUP BY user_id 看似合理,但 status 的值是任意的——它来自该组某一行,数据库不保证是哪一行,MySQL 5.7 默认允许这种“非确定性聚合”,PostgreSQL 则直接报错 column "status" must appear in the GROUP BY clause or be used in an aggregate function。
- 你看到的
status可能是第一条、最后一条,也可能是优化器随机挑的一条,不可依赖 - 哪怕加了
ORDER BY created_at DESC,也不能改变GROUP BY对非分组非聚合字段的未定义行为 - 想“顺便带出其他字段”,唯一正解是先用窗口函数或子查询锁定目标行,再
SELECT *
注意时区、精度和 NULL 值对“最新”的干扰
真实业务中,“最新”往往比想象中脆弱。比如 created_at 是 TIMESTAMP WITHOUT TIME ZONE,应用写入时用的是本地时区,而数据库服务器在 UTC,排序就乱了;或者字段是 DATETIME(3),毫秒级时间在某些客户端显示被截断,肉眼看不出区别但数据库能分出先后。
- 检查
created_at是否允许NULL——NULL在ORDER BY ... DESC中默认排最前还是最后,取决于数据库(PostgreSQL 排最后,MySQL 可能排最前),会导致“最新”被跳过 - 统一用
TIMESTAMP WITH TIME ZONE(PostgreSQL)或显式转成 UTC 存储(MySQL),避免跨时区混乱 - 如果业务上“同秒内只允许一条”,就在应用层加唯一约束
UNIQUE (user_id, DATE(created_at), HOUR(created_at), MINUTE(created_at), SECOND(created_at)),比在 SQL 里硬扛更稳妥
GROUP BY + 非聚合字段的直觉结果——那不是最新,只是碰巧。

















