用 ROW_NUMBER() + PARTITION BY + WHERE rn = 1 实现逻辑去重,按多列分组并依 ORDER BY 规则保留每组首行,可精准控制留哪条;DISTINCT 无法指定保留策略。
怎么用 ROW_NUMBER() 对多列组合去重
直接结论:靠 row_number() + partition by 配合 where rn = 1 实现逻辑去重,不是真删数据,而是筛选出每组的“代表行”。
典型场景是清洗脏数据——比如订单表里 order_id、customer_id、product_id 完全相同的重复记录,只留一条。
-
PARTITION BY后面跟的是“去重维度”,即你认为构成重复的那些列,比如PARTITION BY order_id, customer_id, product_id -
ORDER BY决定哪条被留下:按时间戳升序就留最早的一条,降序就留最新的一条;没业务意义时可固定写ORDER BY NULL(但 Oracle 12c+ 要求必须有ORDER BY,不能真写NULL,得用ORDER BY 1或加个常量) - 别忘了给
ROW_NUMBER()起别名(如rn),否则外层WHERE没法引用
为什么 ROW_NUMBER() 比 DISTINCT 更适合多列去重
DISTINCT 只能返回去重后的完整行,无法控制“留哪一条”;而真实业务中往往要留最新修改的、或状态为 'ACTIVE' 的那条。
比如用户表有 user_id、email、status、updated_at,相同 email 多次注册,你想留 status = 'VERIFIED' 且 updated_at 最大的那条——DISTINCT 做不到,但 ROW_NUMBER() 可以:
SELECT user_id, email, status, updated_at
FROM (
SELECT user_id, email, status, updated_at,
ROW_NUMBER() OVER (
PARTITION BY email
ORDER BY CASE WHEN status = 'VERIFIED' THEN 1 ELSE 0 END DESC,
updated_at DESC
) AS rn
FROM users
)
WHERE rn = 1;
- 这里用了表达式排序:先按验证状态优先级,再按时间,确保业务规则落地
- 注意
CASE表达式必须出现在ORDER BY里,不能只在SELECT列表中 - Oracle 对
ORDER BY子句里的表达式支持良好,但某些旧版本对复杂表达式排序性能较差,可考虑加函数索引
PARTITION BY 写错列会怎样
最常见错误:漏掉关键列,导致本该合并的没合并;或多写了列,把本该保留的拆散了。
例如日志表 log_time、host、service、error_code,你想按“主机+服务+错误码”聚合去重,却只写 PARTITION BY host, service——结果同一台机器上不同错误码也被当成一组,只留一条,丢数据。
- 检查方法:先跑子查询,查几组
COUNT(*)和MIN(log_time), MAX(log_time),确认分组逻辑符合预期 - 如果列里有
NULL,Oracle 默认把所有NULL归为同一组(和 SQL 标准一致),但业务上可能希望NULL不参与分组——得提前用NVL(col, '###')或COALESCE处理 -
PARTITION BY列顺序无关,但建议按选择性从高到低排列(比如先order_id再line_no),对 CBO 优化器更友好
性能和兼容性要注意什么
大表上用 ROW_NUMBER() 去重,本质是全表扫描 + 排序,容易慢,尤其当 PARTITION BY 维度基数低(比如只有几个 status 值),会导致单个分区极大。
- 务必在
PARTITION BY列上建组合索引,最好包含ORDER BY列(如(email, updated_at)),让排序走索引范围扫描 - Oracle 11g 及以前不支持分析函数在
WHERE子句直接使用(会报ORA-30483),必须套一层子查询;12c+ 支持内联视图优化,但语法不变 - 别在 PL/SQL 块里反复执行这种语句——每次都会硬解析;封装成视图或带参数的函数更稳妥
真正麻烦的不是语法,是搞清“哪些列才算重复”——这得翻需求文档、对业务字段含义,而不是看表结构名字就拍板。










