ROW_NUMBER()本身不直接去重,但通过PARTITION BY分组、ORDER BY排序(如created_at DESC)、外层WHERE rn=1筛选,可精准实现“每组留一条”;其严格唯一编号特性优于RANK()/DENSE_RANK(),避免并列导致多行保留。

用 ROW_NUMBER() 配合 WHERE 过滤实现逻辑去重
窗口函数本身不直接“去重”,但 ROW_NUMBER() 可以给重复组内每行打序号,再在外层筛选序号为 1 的记录,达到保留首条的效果。这是最常用、兼容性最好的方案。
- 必须搭配
PARTITION BY指定去重维度(比如按user_id分组) -
ORDER BY决定哪条被保留——若没业务偏好,可用ORDER BY NULL(PostgreSQL/Oracle)或ORDER BY (SELECT NULL)(SQL Server),避免隐式排序开销 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 也支持,但旧版不支持窗口函数
- 示例:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM orders ) t WHERE rn = 1;
DENSE_RANK() 和 RANK() 不适合纯去重
它们会把相同值的行赋予相同排名,导致多行同时满足 =1 条件,无法保证每组只返回一行。
-
DENSE_RANK()在遇到并列时不会跳号,但并列行都得1,过滤后仍可能留多条 -
RANK()同样有并列问题,且后续编号会跳过,更难控制结果集大小 - 除非你明确需要“取所有并列最优者”(比如取每个部门薪资最高的全部员工),否则别用这两个函数做去重
性能和索引要注意什么?
ROW_NUMBER() 的开销主要来自 PARTITION BY 和 ORDER BY 字段的排序。没有索引时,大数据量下可能显著变慢。
- 理想索引是复合索引:
(partition_col, order_col),例如(user_id, created_at) - 如果
ORDER BY用的是表达式(如UPPER(name)),索引可能失效,需谨慎 - 在 PostgreSQL 中,
ORDER BY若含NULLS FIRST/LAST,要确保索引定义一致,否则可能不走索引
替代方案:GROUP BY + 聚合函数能代替吗?
可以,但有局限——只能取聚合结果,不能完整保留某一行原始字段。
- 比如
SELECT user_id, MAX(created_at), MAX(order_amount)能拿到每个用户最新时间及对应最大金额,但无法保证这两字段来自同一行 - 若需整行数据(如最新订单的
order_id、status、amount),GROUP BY无解,必须用窗口函数或子查询 - 某些数据库(如 MySQL 5.7 严格模式)甚至禁止
SELECT *配合GROUP BY,报错Expression #1 of SELECT list is not in GROUP BY clause
实际写的时候,最容易漏掉的是 ORDER BY 的确定性——比如用 created_at 排序,但该字段存在重复值,又没加第二排序键(如 id),就可能导致每次执行结果不一致。

















