如何用SQL窗口函数ROW_NUMBER实现分组内去重
SQL中的ROW_NUMBER()函数常被误会成去重利器,但它其实只负责给行打序号——真正要删除重复行,还得靠外层筛选,比如 WHERE rn = 1。这个道理初看简单,实操时却容易踩坑,特别是排序不稳定或数据库兼容性问题。下面就把这块讲透。

一句话结论: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 LAST或NULLS 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_at
FROM ranked
WHERE rn = 1;
和 DISTINCT、GROUP BY 的关键区别
DISTINCT 是基于整行值去重,GROUP BY 必须搭配聚合函数。而 ROW_NUMBER() 方案的好处是:能「保留原始行的任意字段」——只要排序逻辑设计合理,就能挑出你想要的那条完整记录。
举个例子:按 user_id 分组,取 updated_at 最新的那条,同时还要拿到它的 status、source、id 等字段。GROUP BY 做不到这点(除非把所有字段都写进 GROUP BY,那就失去了去重的意义)。
性能方面:ROW_NUMBER() 需要排序,大数据量时务必关注 PARTITION BY 和 ORDER BY 字段是否有索引。MySQL 8.0+、PostgreSQL、SQL Server 都支持该函数,但 SQLite 目前(3.45 版本)仍不支持窗口函数。
兼容性陷阱:某些还在用旧版 MySQL 的同学可能误以为开窗函数能用,结果报错 ERROR 1064: You ha ve an error in your SQL syntax ——其实就是版本太低,升级到 8.0 以上才能用。
容易被忽略的排序稳定性问题
当 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 最大的那条,结果稳定可预期。
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
配置Java应用支持Oracle数据库AD/Kerberos身份验证
配置Java应用支持OracleAD Kerberos需正确设置JVM的krb5 conf与jaas conf,JDBCURL添加oracle net authentication_services=(KERBEROS5)并与SPN严格匹配。AD用户需映射为Oracle企业用户并授予CREATESESSION权限。SPN注册和EUS映射是常见故障点。
Redis订阅者BGSAVE期间响应变慢:RDB持久化IO影响分析
Redis订阅者在BGSAVE期间响应变慢,源于fork阻塞、COW内存争抢及磁盘I O高负载三重叠加。通过将RDB迁移至从节点、禁用THP、调整swappiness并手动触发避开高峰,可有效降低对主服务的影响。
如何优雅捕获Node.js项目中的MongoDB写入异常
批量插入失败时不会抛出统一错误类型,需关注错误代码和写入错误数组。部分失败时可设置有序参数为假以获取写入错误数组。事务中写入失败会自动中止事务,提交事务不会执行。常见错误码11000为唯一键冲突,121为文档验证失败。
如何用SQL窗口函数ROW_NUMBER实现分组内去重
行号函数仅分配行序号,不能直接去重。需通过子查询或公用表表达式过滤行号为1的行,实现分组内去重。若排序字段有重复,应加入唯一键以保证排序稳定性。还需注意不同数据库对空值排序的差异及版本兼容性。
Spring Boot RedisTemplate无法直接注入?检查泛型声明是否正确
RedisTemplate注入失败多因泛型擦除导致类型不匹配,需保证配置类返回类型、注入字段类型及内部key value类型三者一致。避免使用无泛型或通配符形式,不同序列化策略应定义独立Bean并用@Qualifier区分。StringRedisTemplate为独立Bean,不可混用。
- 日榜
- 周榜
- 月榜
相关攻略
2026-07-08 07:04
2026-07-08 07:03
2026-07-08 07:03
2026-07-08 07:03
2026-07-08 07:03
2026-07-08 07:02
2026-07-08 07:02
2026-07-08 07:02
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程
热门话题

