如何使用CTE和ROW_NUMBER函数在SQL存储过程中巧妙删除重复数据?

2026-04-29 01:152阅读0评论SEO基础
  • 内容介绍
  • 相关推荐

本文共计730个文字,预计阅读时间需要3分钟。

如何使用CTE和ROW_NUMBER函数在SQL存储过程中巧妙删除重复数据?

直接删除重复数据,使用CTE和ROW_NUMBER()函数最稳妥——它能精确保留每组的第一条记录,且不依赖任何单一键。

为什么不能直接 DELETEGROUP 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 BYORDER 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 colsDELETE WHERE id NOT IN (SELECT id FROM #keep),但隐患明显:

  • 若原表无主键或 id 不唯一,MIN(id) 可能选错行
  • NOT INNULL 直接返回空结果集,整条 DELETE 不生效(静默失败)
  • CTE 方案原子性强,逻辑集中,执行计划更可控

真正难的不是写对语法,而是确认「哪些列定义重复」和「保留哪一条」的业务规则——这个必须和产品/业务方对齐,代码只是执行工具。

本文共计730个文字,预计阅读时间需要3分钟。

如何使用CTE和ROW_NUMBER函数在SQL存储过程中巧妙删除重复数据?

直接删除重复数据,使用CTE和ROW_NUMBER()函数最稳妥——它能精确保留每组的第一条记录,且不依赖任何单一键。

为什么不能直接 DELETEGROUP 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 BYORDER 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 colsDELETE WHERE id NOT IN (SELECT id FROM #keep),但隐患明显:

  • 若原表无主键或 id 不唯一,MIN(id) 可能选错行
  • NOT INNULL 直接返回空结果集,整条 DELETE 不生效(静默失败)
  • CTE 方案原子性强,逻辑集中,执行计划更可控

真正难的不是写对语法,而是确认「哪些列定义重复」和「保留哪一条」的业务规则——这个必须和产品/业务方对齐,代码只是执行工具。