如何使用SQL窗口函数ROW_NUMBER在分组内实现去重?

作者:袖梨 2026-07-08
ROW_NUMBER() 不能直接去重,仅能编号;真正去重需配合子查询或CTE筛选rn=1的行,核心是PARTITION BY定义重复组、ORDER BY确保排序稳定,否则结果不可控。

ROW_NUMBER() 不能直接去重,它只能给行打序号;真正去重得靠外部筛选,比如 WHERE rn = 1。

为什么 ROW_NUMBER() 本身不等于去重

它只是按指定排序规则给每一行分配一个唯一递增数字,哪怕两行数据完全相同,只要排序键(ORDER BY)能区分它们(或用了不确定排序),ROW_NUMBER() 也会分出 1、2、3…… 所以必须配合子查询或 CTE,再过滤出序号为 1 的行。

常见错误现象:SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) FROM orders; 这条语句只是加了一列序号,并没有减少行数。

标准写法:用 CTE 或子查询 + WHERE 筛选首行

这是最常用、也最易理解的模式。核心是先算出每组内的序号,再在外层只取 rn = 1 的记录。

  • 如果想保留最新一条记录:用 ORDER BY updated_at DESC,然后取 rn = 1
  • 如果想保留最早一条:用 ORDER BY created_at ASC
  • 注意:如果排序字段有 NULL,不同数据库行为不同(如 PostgreSQL 把 NULL 排在最前,MySQL 可能默认最后),建议显式写 NULLS LASTNULLS FIRST(PostgreSQL/Oracle 支持;MySQL 8.0+ 不支持该语法,需用 IS NULL 模拟)
WITH ranked AS (  SELECT *,         ROW_NUMBER() OVER (           PARTITION BY user_id            ORDER BY updated_at DESC, id DESC         ) AS rn  FROM users)SELECT id, user_id, email, updated_atFROM rankedWHERE rn = 1;

和 DISTINCT、GROUP BY 的关键区别

DISTINCT 是基于整行值去重,GROUP BY 必须配聚合函数,而 ROW_NUMBER() 方案能「保留原始行的任意字段」——只要你排序逻辑合理,就能挑出你想要的那条完整记录。

  • 例如:按 user_id 分组,取 updated_at 最新那条,同时拿到它的 statussourceid 等所有字段,GROUP BY 做不到这点(除非全写进 GROUP BY,那就失去去重意义)
  • 性能上:ROW_NUMBER() 需要排序,大数据量时注意 PARTITION BYORDER BY 字段是否有索引;MySQL 8.0+、PostgreSQL、SQL Server 都支持,但 SQLite 目前(3.45 仍不支持窗口函数)
  • 兼容性陷阱:某些旧版 MySQL 用户误以为开窗函数可用,结果报错 ERROR 1064: You have an error in your SQL syntax,其实是版本太低

容易被忽略的排序稳定性问题

PARTITION BY 内多行的排序字段值完全相同时(比如同个 user_id 下几条记录 updated_at 都是 '2024-01-01 00:00:00'),ROW_NUMBER() 的结果是不确定的——数据库可能每次返回不同“首行”。

解决办法只有一个:ORDER BY 里补一个能保证唯一性的字段(如主键 id,否则去重结果不可复现。

错误写法:ORDER BY updated_at DESC → 同时间戳下哪条被选中是随机的
正确写法:ORDER BY updated_at DESC, id DESC → 时间相同时,取 id 最大的那条,稳定可预期

相关文章

精彩推荐