row_number()仅生成唯一行号,不删除数据;真正去重需结合子查询或cte筛选rn=1的行,如with ranked as (select , row_number() over (partition by email order by created_at desc) as rn from users) select from ranked where rn = 1。

ROW_NUMBER() 本身不删数据,只标记行号
很多人以为 ROW_NUMBER() 能直接“去重”,其实它只是给每一行按规则打一个序号。真正去重得靠这个序号配合 WHERE 或子查询筛选——比如只保留每个分组里的第一行。
典型误区是写成:SELECT DISTINCT *, ROW_NUMBER() OVER (...) FROM ...,这毫无意义:DISTINCT 和窗口函数不能混用,SQL 会报错或行为不可控。
- 必须把
ROW_NUMBER()放在子查询或 CTE 里生成序号列 - 分区字段(
PARTITION BY)要选真正定义“重复”的依据,比如email、order_id,而不是主键 - 排序字段(
ORDER BY)决定哪一行被保留:想留最新记录就按时间倒序,想留最早就正序
标准写法:用 CTE + WHERE 过滤重复行
这是最清晰、兼容性最好的方式,适用于 PostgreSQL、SQL Server、Oracle、MySQL 8.0+:
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY email ORDER BY created_at DESC
) AS rn
FROM users
)
SELECT id, email, created_at
FROM ranked
WHERE rn = 1;
注意:PARTITION BY email 表示“相同邮箱视为一组”,ORDER BY created_at DESC 让最新注册的排第一,rn = 1 就只取每组头一条。
- 如果表没主键或时间字段,可用
ORDER BY id保底,但结果不稳定(不同数据库可能返回不同行) - 别在
WHERE里直接写ROW_NUMBER() > 1—— 窗口函数不能出现在 WHERE 子句中 - PostgreSQL 允许在子查询里用
SELECT *,但 SQL Server 要显式列出字段,避免 CTE 中出现重复列名
DELETE 重复行时,必须用主键或唯一标识定位
想真正删数据?不能直接 DELETE FROM ... WHERE rn > 1,因为 rn 是计算列,不在原表里。得先拿到要删的行 ID:
DELETE FROM users
WHERE id NOT IN (
SELECT id FROM (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY email ORDER BY created_at DESC
) AS rn
FROM users
) ranked
WHERE rn = 1
);
这个写法看似绕,但安全:它靠 id 定位,不会误删整组。
- 千万避免
DELETE FROM users WHERE email IN (...)这种写法——它会删掉所有同邮箱记录,只剩一条也不剩 - MySQL 5.7 或更早版本不支持在子查询中引用同一张表,得加一层包装:
SELECT * FROM (SELECT ...) AS tmp - 大表执行前务必加
EXPLAIN看是否命中索引;PARTITION BY字段最好有索引,否则性能断崖下跌
替代方案:GROUP BY + 聚合函数更轻量,但有局限
如果只要查去重后的某几个字段(比如只取每个邮箱的最新创建时间),GROUP BY 加 MAX(created_at) 比 ROW_NUMBER() 更快,也更易读:
SELECT email, MAX(created_at) AS latest_time FROM users GROUP BY email;
但它没法带回整行数据(比如对应那条记录的 id 或 name),除非用 JOIN 回原表,反而更复杂。
-
ROW_NUMBER()的核心价值是“保留完整原始行”,而不仅是聚合统计 - 如果业务只要去重结果集展示,不用改原表,优先考虑
GROUP BY;如果要导出/同步/清理,还是用带ROW_NUMBER()的 CTE 更稳妥 - SQLite 直到 3.25.0 才支持窗口函数,老版本只能用关联子查询模拟,性能差很多
实际执行时最容易卡在分区字段没索引,或者误把 ORDER BY 写成 created_at ASC 却以为留的是最新数据。










