row_number() 是最可靠的每组取最新一条的标准解法,需在子查询中用 partition by 和 order by time desc, id desc 实现,避免 where 直接调用窗口函数。

用 ROW_NUMBER() 按组排序取第一条最可靠
绝大多数场景下,ROW_NUMBER() 是唯一能稳定获取“每组最新一条”的标准解法。它不依赖时间字段是否严格唯一,也不怕重复值或空值干扰。
常见错误是直接 GROUP BY + MAX(time) 再关联原表——一旦某组存在多条记录时间相同,就会漏掉或返回意外行。
- 写法核心:在子查询或 CTE 中用
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY time DESC) - 务必用
ORDER BY time DESC, id DESC补充二级排序(防止时间相同时结果不确定) - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 也支持
- 避免在
WHERE中直接写ROW_NUMBER() = 1—— 必须套一层子查询,因为窗口函数不能在 WHERE 执行
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY created_at DESC, id DESC
) rn
FROM orders
) t WHERE rn = 1;
MySQL 5.7 或旧版本只能靠自连接或子查询
没有窗口函数时,ROW_NUMBER() 不可用,必须用关联或相关子查询,性能差且易出错。
典型陷阱是用 WHERE (group_col, time) IN (SELECT group_col, MAX(time) ...) —— 如果 time 为 NULL,整行被忽略;若多行时间相同,可能返回多条。
- 安全写法是自连接:找不出比自己更新的同组记录
-
LEFT JOIN后WHERE t2.id IS NULL表示该行是组内最新 - 索引必须包含
(group_col, time)或(group_col, time, id),否则全表扫描 - 如果
time允许 NULL,要额外处理:AND t2.time
SELECT t1.* FROM orders t1
LEFT JOIN orders t2 ON t1.user_id = t2.user_id
AND (t2.created_at > t1.created_at
OR (t2.created_at = t1.created_at AND t2.id > t1.id))
WHERE t2.id IS NULL;
SELECT DISTINCT ON 是 PostgreSQL 的快捷方案
PostgreSQL 提供语法糖 DISTINCT ON,写起来短,但仅限 PG,且行为和 ROW_NUMBER() 有细微差别。
它按 DISTINCT ON (col) 分组后,只保留每个分组中 ORDER BY 第一个出现的行——不保证“最新”,只保证“排序后第一个”。
- 必须紧跟
ORDER BY,且DISTINCT ON列必须是ORDER BY的前缀 - 写成
SELECT DISTINCT ON (user_id) * FROM orders ORDER BY user_id, created_at DESC, id DESC - 如果漏写
id DESC,当时间相同时,返回哪一行不可控 - 不能用于 UPDATE / DELETE,也不能嵌套在子查询里直接加 WHERE 过滤
别踩这些坑
“最新”不等于“时间最大”,尤其当业务逻辑里存在状态覆盖、软删除、或时间由客户端生成时。
-
created_at字段没索引?查询会慢十倍以上,尤其数据量过百万 - 用
datetime而不是timestamp?注意时区转换可能导致排序错乱 - 硬编码
ORDER BY created_at DESC但字段允许 NULL?NULL 默认排最前,最新记录反而被跳过 - 在视图里封装这个逻辑?确认下游应用是否依赖确定性结果——窗口函数在某些执行计划下可能产生非预期顺序
真正难的不是写出语句,而是确认“最新”的业务定义是否和 SQL 行为一致。比如审核通过时间、最后编辑时间、消息接收时间,各自对应的字段和约束完全不同。











