
直接用 ROW_NUMBER() 配合 CTE 是目前 SQL Server 中最稳妥、可控性最强的去重方式,尤其适合需要「保留最新/最早记录」的生产场景。它不依赖临时表,不破坏原有表结构,还能精确控制排序逻辑。
为什么必须用 CTE 包裹 ROW_NUMBER() 才能 DELETE
SQL Server 不允许在 DELETE 语句中直接对窗口函数结果做操作,比如下面这句会报错:
DELETE FROM your_table
WHERE id IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (PARTITION BY col1, col2 ORDER BY id) AS rn
FROM your_table
) t WHERE rn > 1
);错误信息是:Invalid column name 'rn' 或更常见的 Cannot specify target table 'your_table' for update in FROM clause。根本原因是子查询里不能同时读写同一张表。
CTE(公用表表达式)绕过了这个限制,它把窗口计算结果当成一个“逻辑视图”,让 DELETE 有明确的别名目标可操作。
关键点:
- CTE 必须带别名(哪怕只是
AS t),否则DELETE FROM t会失败 -
ROW_NUMBER()的PARTITION BY要严格对应你定义的“重复依据列” -
ORDER BY决定保留哪条——比如按时间倒序就保留最新,按主键正序就保留最早
保留最新记录:按 datetime 或 identity 列倒序
业务中最常见需求:相同业务键(如 user_id, order_id)只留最后一条。这时 ORDER BY created_at DESC 或 ORDER BY id DESC 是关键。
示例(删除 orders 表中重复的 user_id + product_id 组合,保留 created_at 最大的那条):
WITH duplicates AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY user_id, product_id
ORDER BY created_at DESC
) AS rn
FROM orders
)
DELETE FROM duplicates WHERE rn > 1;注意:
- 如果
created_at允许为NULL,ORDER BY created_at DESC会让NULL排最前,可能误删——建议加IS NULL处理或改用id - 若表无时间字段,且
id是自增主键,则ORDER BY id DESC是安全替代 - 执行前务必先用
SELECT * FROM duplicates WHERE rn > 1预览将删哪些行
删除前没加 WHERE 条件导致全表清空
这是真实踩过的坑:有人复制了 CTE 模板,但漏写了 WHERE rn > 1,结果执行了:
WITH duplicates AS (...) DELETE FROM duplicates;
——等价于 DELETE FROM orders,整张表没了。
安全习惯:
- 永远先运行
SELECT版本确认数据范围 - 在事务里执行:
BEGIN TRAN→DELETE→SELECT COUNT(*)→COMMIT或ROLLBACK - 避免直接在生产环境跑未验证的
DELETE,哪怕只是测试语句
性能与锁表现:大表要小心
ROW_NUMBER() + CTE 删除在百万级数据上通常比临时表方案快,但仍有明显锁表现:
- 整个 CTE 扫描过程会持有
SCH-S(架构稳定锁),阻塞 DDL 操作 - 实际删除阶段会升级为
KEY或HOBT锁,可能阻塞其他UPDATE/DELETE - 如果分区键列没有索引,
PARTITION BY会导致全表扫描,非常慢
优化建议:
- 确保
PARTITION BY列上有合适索引(比如复合索引(col1, col2, order_col)) - 超大表(千万级以上)考虑分批删:加
TOP (10000)和循环,或用WHERE id BETWEEN ...分段 - 避免在业务高峰期执行,尤其是 OLTP 核心表
真正麻烦的不是语法写不对,而是没想清楚「重复」到底由哪些字段定义、该按什么逻辑保留——这些一旦错,删掉的就是正确数据。动手前花十分钟画个业务逻辑图,比调半小时 SQL 更有效。

















