ROW_NUMBER() 必须配合CTE使用,不能直接在DELETE中嵌套;正确写法是WITH CTE AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) AS rn FROM t) DELETE FROM CTE WHERE rn > 1。

ROW_NUMBER() 必须配合 CTE 使用,不能直接在 DELETE 中嵌套子查询
SQL Server 不允许在 DELETE 语句中直接使用窗口函数(比如 ROW_NUMBER())作为过滤条件。你写 DELETE FROM t WHERE ROW_NUMBER() OVER (...) > 1 会报错:“窗口函数只能出现在 SELECT 或 ORDER BY 子句中”。正确路径是先用 WITH CTE AS (...) 构造带行号的临时结果集,再对 CTE 执行删除。
常见错误现象:
Msg 4108, Level 15: Windowed functions can only appear in the SELECT or ORDER BY clauses.- 误以为
SELECT * FROM (SELECT *, ROW_NUMBER() ...) t WHERE rn > 1的结果可以直接喂给DELETE—— 实际上这是非法语法,必须走 CTE 或派生表 + JOIN 路径
实操建议:
- 始终以
WITH CTE AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY col1, col2 ORDER BY create_time DESC) AS rn FROM your_table)开头 -
DELETE FROM CTE WHERE rn > 1是合法且最简写法(注意:CTE 在此上下文中可被修改,前提是它引用单个基表) - 如果表有触发器或外键约束,CTE 删除仍会触发它们;若需绕过,得改用
DELETE t FROM your_table t INNER JOIN ...模式
PARTITION BY 字段组合必须严格匹配业务去重定义
重复不是技术概念,而是业务规则。比如订单去重,PARTITION BY order_sn 和 PARTITION BY customer_id, product_id, DATEADD(second, -5, create_time) 产生的分组完全不同。
容易踩的坑:
- 用
PARTITION BY email清理用户表,却忽略大小写差异 ——'A@B.COM'和'a@b.com'会被视为两组,漏删 - 时间字段未截断精度:订单创建时间含毫秒,
create_time直接参与PARTITION BY会导致几乎无重复 —— 应改用CAST(create_time AS DATETIME2(0))或CONVERT(DATE, create_time) - NULL 值在
PARTITION BY中默认不相等:两行external_order_id IS NULL不会被分到同一组,需提前用ISNULL(external_order_id, 'N/A')统一占位
性能影响:
- 分区字段越多、越宽(如长文本),排序开销越大;优先选高基数、窄字段(如
INT主键)做分区依据 - ORDER BY 若涉及非索引列(如
update_time DESC),可能触发大范围排序,建议在该列建索引
删除前务必验证 CTE 查询结果,且禁止跳过备份与事务封装
直接执行 DELETE FROM CTE WHERE rn > 1 是高危操作。哪怕逻辑看似正确,也可能因分区逻辑偏差导致误删。
实操建议:
- 先运行
SELECT * FROM CTE WHERE rn > 1,人工抽查 10–20 条,确认它们确实是冗余项(比如相同order_sn下create_time更早、状态更旧) - 所有生产环境操作必须包裹在显式事务中:
BEGIN TRAN; DELETE ...; IF @@ROWCOUNT = X SELECT 'OK' ELSE ROLLBACK; COMMIT; - 执行前备份关键字段快照:
SELECT order_sn, customer_id, create_time, status INTO #backup_dup BEFORE DELETE,而非依赖全库备份(恢复粒度太大) - 避免用
TOP N分批删除时忽略排序稳定性:加ORDER BY (SELECT NULL)会导致每次执行删的行不一致,应明确ORDER BY order_sn, create_time
遇到 IDENTITY 列或复杂约束时,CTE 删除可能失效
当目标表含 IDENTITY 列、启用变更数据捕获(CDC)、或存在级联外键时,DELETE FROM CTE 可能失败或行为异常。
替代方案:
- 改用自连接删除:
DELETE t1 FROM your_table t1 INNER JOIN your_table t2 ON t1.dup_key = t2.dup_key AND t1.id > t2.id(需确保id是唯一递增标识) - 对 CDC 启用的表,先停用 CDC,删完再启用;否则
DELETE会写入变更表,放大日志体积 - 若表有
INSTEAD OF DELETE触发器,CTE 删除会被拦截 —— 此时必须查触发器逻辑,或改用DELETE FROM your_table WHERE id IN (SELECT id FROM CTE WHERE rn > 1)
真正麻烦的不是语法,而是“哪几条该留、哪几条该删”这个判断本身。业务方没说清保留策略前,任何 ORDER BY create_time DESC 都只是假设。

















