如何使用CTE和ROW_NUMBER函数在SQL存储过程中巧妙删除重复数据?
- 内容介绍
- 相关推荐
本文共计730个文字,预计阅读时间需要3分钟。
直接删除重复数据,使用CTE和ROW_NUMBER()函数最稳妥——它能精确保留每组的第一条记录,且不依赖任何单一键。
为什么不能直接 DELETE 加 GROUP BY?
SQL Server(及多数主流数据库)不允许在 DELETE 语句中直接使用 GROUP BY 或聚合函数。硬写 DELETE FROM t GROUP BY col 会报错:Incorrect syntax near the keyword 'GROUP'。CTE 提供了一个可更新的“逻辑视图”,让 ROW_NUMBER() 的结果能被 DELETE 引用。
ROW_NUMBER() 必须配合 PARTITION BY 和 ORDER BY
只写 ROW_NUMBER() OVER () 没意义——它会给全表所有行统一编号(1 到 N),起不到“按重复组划分”的作用。关键在两个子句:
-
PARTITION BY col1, col2:定义“哪些列相同算重复”,即分组依据 -
ORDER BY id ASC:决定每组内哪条被保留(通常选最小id,或最新时间戳)
示例(删除 users 表中 email 相同的重复记录,保留 id 最小的):
WITH dup AS ( SELECT id, email, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users ) DELETE FROM dup WHERE rn > 1;
执行前务必加事务和验证
这条语句是真删,不可逆。常见疏漏包括:
- 忘了改
ORDER BY方向,导致留错行(比如用ORDER BY id DESC就会删掉旧数据,留下新数据) - 没检查
PARTITION BY列是否含NULL:SQL 中NULL = NULL为 false,多行NULL不会被分到同一组——若业务认为NULL算相同值,需提前用ISNULL(email, '')处理 - 未用
SELECT * FROM dup WHERE rn > 1先预览将删哪些行 - 生产环境没套事务:
BEGIN TRAN; ... DELETE ...; ROLLBACK;测试完再COMMIT
替代方案对比:临时表 or GROUP BY + 子查询?
有人用 SELECT MIN(id) INTO #keep FROM t GROUP BY cols 再 DELETE WHERE id NOT IN (SELECT id FROM #keep),但隐患明显:
- 若原表无主键或
id不唯一,MIN(id)可能选错行 -
NOT IN遇NULL直接返回空结果集,整条DELETE不生效(静默失败) - CTE 方案原子性强,逻辑集中,执行计划更可控
真正难的不是写对语法,而是确认「哪些列定义重复」和「保留哪一条」的业务规则——这个必须和产品/业务方对齐,代码只是执行工具。
本文共计730个文字,预计阅读时间需要3分钟。
直接删除重复数据,使用CTE和ROW_NUMBER()函数最稳妥——它能精确保留每组的第一条记录,且不依赖任何单一键。
为什么不能直接 DELETE 加 GROUP BY?
SQL Server(及多数主流数据库)不允许在 DELETE 语句中直接使用 GROUP BY 或聚合函数。硬写 DELETE FROM t GROUP BY col 会报错:Incorrect syntax near the keyword 'GROUP'。CTE 提供了一个可更新的“逻辑视图”,让 ROW_NUMBER() 的结果能被 DELETE 引用。
ROW_NUMBER() 必须配合 PARTITION BY 和 ORDER BY
只写 ROW_NUMBER() OVER () 没意义——它会给全表所有行统一编号(1 到 N),起不到“按重复组划分”的作用。关键在两个子句:
-
PARTITION BY col1, col2:定义“哪些列相同算重复”,即分组依据 -
ORDER BY id ASC:决定每组内哪条被保留(通常选最小id,或最新时间戳)
示例(删除 users 表中 email 相同的重复记录,保留 id 最小的):
WITH dup AS ( SELECT id, email, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users ) DELETE FROM dup WHERE rn > 1;
执行前务必加事务和验证
这条语句是真删,不可逆。常见疏漏包括:
- 忘了改
ORDER BY方向,导致留错行(比如用ORDER BY id DESC就会删掉旧数据,留下新数据) - 没检查
PARTITION BY列是否含NULL:SQL 中NULL = NULL为 false,多行NULL不会被分到同一组——若业务认为NULL算相同值,需提前用ISNULL(email, '')处理 - 未用
SELECT * FROM dup WHERE rn > 1先预览将删哪些行 - 生产环境没套事务:
BEGIN TRAN; ... DELETE ...; ROLLBACK;测试完再COMMIT
替代方案对比:临时表 or GROUP BY + 子查询?
有人用 SELECT MIN(id) INTO #keep FROM t GROUP BY cols 再 DELETE WHERE id NOT IN (SELECT id FROM #keep),但隐患明显:
- 若原表无主键或
id不唯一,MIN(id)可能选错行 -
NOT IN遇NULL直接返回空结果集,整条DELETE不生效(静默失败) - CTE 方案原子性强,逻辑集中,执行计划更可控
真正难的不是写对语法,而是确认「哪些列定义重复」和「保留哪一条」的业务规则——这个必须和产品/业务方对齐,代码只是执行工具。

