ROW_NUMBER() 是去重保最新最常用的选择,因其可在分组内按时间倒序编号后筛选序号为1的完整记录,逻辑清晰且主流数据库均支持;需配合OVER()中ORDER BY指定排序,重复时间时应加二级排序防不稳定。

为什么 ROW_NUMBER() 是去重保最新最常用的选择
直接用 GROUP BY 无法保留“最新”那条完整记录,而 DISTINCT 又不认时间字段的大小关系。窗口函数中 ROW_NUMBER() 能按分组内时间倒序编号,再筛出序号为 1 的行,逻辑清晰、兼容性好(MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持)。
- 必须配合
ORDER BY子句在OVER()中指定排序依据,比如created_at DESC或id DESC - 如果时间字段有重复,建议加二级排序(如
ORDER BY updated_at DESC, id DESC),避免结果不稳定 -
RANK()和DENSE_RANK()会把相同时间的多条都标为 1,达不到“只留一条”的目的
实际写法:子查询 + ROW_NUMBER() 筛选
核心是先在子查询或 CTE 中计算行号,外层过滤 rn = 1。不能在同一个查询层级里 WHERE 引用窗口函数结果。
SELECT id, user_id, status, updated_at
FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY updated_at DESC, id DESC
) AS rn
FROM orders
) t
WHERE rn = 1;
-
PARTITION BY user_id表示按用户分组去重;换成product_id就是按商品去重 -
ORDER BY updated_at DESC, id DESC确保更新时间最新且 ID 最大的那条胜出 - 别忘了给子查询起别名(如
t),否则 MySQL 会报错
MySQL 5.7 或更老版本怎么办
这些版本不支持窗口函数,得用自连接或相关子查询模拟。性能差、写法绕,但有时不得不做:
SELECT o1.*
FROM orders o1
WHERE o1.updated_at = (
SELECT MAX(o2.updated_at)
FROM orders o2
WHERE o2.user_id = o1.user_id
)
AND o1.id = (
SELECT MAX(o3.id)
FROM orders o3
WHERE o3.user_id = o1.user_id
AND o3.updated_at = o1.updated_at
);
- 这个写法假设“同用户同时间有多条时,取 id 最大的那条”
- 没有索引的话,
user_id + updated_at组合索引能大幅提速 - 如果数据量大,这种写法可能比窗口函数慢一个数量级,别在线上表盲目套用
容易被忽略的 NULL 和时区问题
updated_at 为 NULL 的记录会被排在最前面(多数数据库默认 NULLS FIRST),导致它们意外中标。还有时区不一致会让“最新”判断出错。
- 在
ORDER BY中显式控制 NULL:加NULLS LAST(PostgreSQL、Oracle 支持),MySQL 不支持,得提前用COALESCE(updated_at, '1970-01-01')处理 - 确保所有时间字段已转为统一时区(如 UTC),否则跨服务器写入的数据可能因本地时区不同导致排序错乱
- 如果业务要求“最后插入的那条”,优先用自增
id排序,它比时间字段更可靠










