必须用CTE包裹ROW_NUMBER()标识重复组内序号,再删rn>1的行;PARTITION BY需包含所有判重列,ORDER BY须含唯一列(如id)确保排序稳定,DELETE必须直接作用于CTE。

用CTE + ROW_NUMBER() 删除重复行,核心是标出“该留哪一条”
直接结论:不能靠 DELETE 单独删,必须用 CTE 包裹 ROW_NUMBER() 生成序号,再删掉序号 > 1 的行。关键不是“去重”,而是“明确保留逻辑”——比如按主键升序留最小 ID 的那条。
CTE里ROW_NUMBER()的PARTITION BY和ORDER BY怎么写才对
错配 PARTITION BY 和 ORDER BY 是最常见翻车点。前者定义“哪些行算一组重复”,后者决定“组内哪条排第一”。两者必须严格对应业务需求:
-
PARTITION BY填所有判定重复的列,比如PARTITION BY name, email,不是只填name -
ORDER BY必须包含能唯一区分行的列(通常是主键或时间戳),比如ORDER BY id ASC或ORDER BY created_at DESC;只写ORDER BY name会导致同组内排序不稳定,ROW_NUMBER()结果不可控 - 如果表没主键,得先加
ORDER BY (SELECT NULL)强制稳定(但不推荐,应优先补主键)
DELETE语句必须嵌套在CTE中,不能直接对ROW_NUMBER()结果操作
SQL Server 和 PostgreSQL 支持 CTE 后紧跟 DELETE,但语法限制很死:CTE 必须是语句第一个部分,且 DELETE 的 FROM 必须引用该 CTE 名。常见错误包括:
- 在 CTE 外再写
DELETE FROM t WHERE id IN (SELECT id FROM cte WHERE rn > 1)—— 这会多查一次,且可能误删(若 CTE 中没选id) - 把
ROW_NUMBER()写在子查询里然后DELETE—— 多数数据库不支持直接删子查询结果集 - MySQL 8.0+ 虽支持 CTE,但旧版(5.7)完全不支持,得改用临时表或自连接
正确写法示例(SQL Server):
WITH dup AS (
SELECT id, name, email,
ROW_NUMBER() OVER (PARTITION BY name, email ORDER BY id ASC) AS rn
FROM users
)
DELETE FROM dup WHERE rn > 1;
执行前务必先SELECT验证,尤其注意NULL值是否被当作相同值
PARTITION BY 中任意列为 NULL 时,SQL 标准规定它们彼此相等——也就是说,name = 'Alice' 且 email IS NULL 的两行会被判为重复。这常被忽略,导致误删。
- 先跑一遍
SELECT * FROM dup WHERE rn > 1,确认要删的确实是预期重复行 - 如果业务上
NULL不代表相同(比如 email 缺失 ≠ 邮箱相同),得在PARTITION BY中显式处理,例如:PARTITION BY ISNULL(name, ''), ISNULL(email, '') - 大表操作前加事务,
BEGIN TRAN→ 执行 →SELECT COUNT(*)确认删了多少 → 再COMMIT或ROLLBACK
真正麻烦的从来不是语法,是搞清“哪些字段组合才算重复”以及“同一组里哪条数据有资格活下来”。这两个问题没想透,ROW_NUMBER() 写得再顺也没用。










