SQL Server 存储过程必须用 CTE 包裹 ROW_NUMBER() 才能 DELETE,因直接对子查询派生表执行 DELETE 语法不被允许,会报错 Msg 4145 或 208;CTE 提供事务边界、可读性及中间结果验证能力。

必须用 CTE 包裹 ROW_NUMBER(),否则存储过程中直接 DELETE 子查询会报错
为什么存储过程里不能直接写 DELETE FROM (SELECT ..., ROW_NUMBER() ...)
SQL Server 不允许对派生表(包括子查询、视图、内联表表达式)执行 DELETE。存储过程里若漏掉 CTE 层,语句会直接失败,报错信息是:Msg 4145, Level 15, State 1: An expression of non-boolean type specified in a context where a condition is expected 或更常见的 Msg 208, Level 16: Invalid object name——本质是语法不被接受,不是权限或对象问题。
CTE 不仅是语法绕过手段,更是事务边界和可读性的锚点:它让“先标号、再删”的逻辑显性化,方便加 PRINT 或临时 SELECT 验证中间结果。
- 错误写法:
DELETE FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY a,b ORDER BY c DESC) AS rn FROM t) WHERE rn > 1 - 正确结构:必须以
WITH cte AS (...)开头,再DELETE FROM cte WHERE rn > 1 - 存储过程里建议加
SET NOCOUNT ON,避免客户端被大量(X rows affected)消息干扰
PARTITION BY 和 ORDER BY 怎么选才不丢数据
去重逻辑完全由这两个子句定义:PARTITION BY 决定“哪些行算一组”,ORDER BY 决定“组内留哪条”。业务没对齐就写,极易留错记录。
例如用户表 users(id, email, phone, updated_at),业务规则是“邮箱唯一,保留最新更新的那条”,则:
-
PARTITION BY email—— 必须排除NULL,否则所有空邮箱被归为一组;可加WHERE email IS NOT NULL预过滤 -
ORDER BY updated_at DESC, id DESC——updated_at相同时靠id保稳定,避免因时钟精度导致排序不确定 - 严禁用
ORDER BY (SELECT NULL),它在存储过程多次执行中可能返回不同rn=1的行,造成非预期丢失
大表去重怎么避免锁表太久
千万级表上跑 DELETE,默认走表锁或页锁,可能阻塞下游查询数分钟。关键不是“快”,而是“可控”。
- 分批删:用
TOP (10000)+WHILE @@ROWCOUNT > 0循环,每次只删固定行数,配合WAITFOR DELAY '00:00:01'减轻日志压力 - 删前建索引:确保
PARTITION BY字段组合上有索引(如INDEX IX_users_email ON users(email)),否则ROW_NUMBER()排序开销爆炸 - 删后立刻执行:
UPDATE STATISTICS users WITH FULLSCAN,否则后续查询可能沿用过期统计,引发低效执行计划 - 别依赖
TRUNCATE:它不能带WHERE,无法实现“留最新、删旧”这类条件去重
验证重复分布比直接删更重要
存储过程一旦提交,回滚成本极高。上线前必须用只读查询确认重复模式,而不是凭经验假设。
在存储过程开头加一段验证逻辑(可注释掉上线后):
SELECT COUNT(*) AS total_dup_groups, MIN(cnt) AS min_dup_per_group, MAX(cnt) AS max_dup_per_group, AVG(cnt * 1.0) AS avg_dup_per_group FROM ( SELECT COUNT(*) AS cnt FROM users WHERE email IS NOT NULL GROUP BY email HAVING COUNT(*) > 1 ) t;
如果 max_dup_per_group 超过 100,说明存在异常聚集(比如埋点错误或补录失控),此时不应直接删,而该触发告警或人工介入。真正危险的不是重复本身,而是重复背后的系统性失稳。

















