不能直接用DELETE删除百万行数据,因会引发长时间锁表、全表扫描、binlog/undo log暴涨、主从延迟加剧及事务超时;必须加索引并分批删除。

为什么不能直接用 DELETE 删除百万行数据
直接执行 DELETE FROM Orders WHERE CreatedDate 会一次性申请大量事务日志空间,可能触发日志满、锁表超时或阻塞其他业务。SQL Server 默认以单事务执行,日志无法截断,且行锁可能升级为表锁——这不是“删得慢”,而是“删到一半就卡死”。
TOP + 子查询实现可控分批删除
核心思路是:每次只删固定行数(如 5000 行),用子查询定位待删范围,再用 TOP 限制数量,循环执行直到无匹配数据。
- 必须用子查询包裹
TOP,否则TOP在DELETE中不支持ORDER BY(而你通常需要按时间/主键有序删除) - 推荐写法:
DELETE TOP (5000) FROM Orders WHERE ID IN (SELECT TOP 5000 ID FROM Orders WHERE CreatedDate - 注意:
IN子查询里必须显式指定ORDER BY,否则删的可能是任意 5000 行,破坏顺序性与可预测性 - 若表有聚集索引(尤其主键是自增
ID),该写法能利用索引快速定位,避免全表扫描
用 CTE + ROW_NUMBER() 更安全地分页删除
当删除条件涉及多列、非索引字段,或需严格按业务逻辑顺序(比如按 LastModified 时间+版本号组合去重)时,CTE 是更可靠的选择。
- 示例:
WITH cte AS (SELECT *, ROW_NUMBER() OVER (ORDER BY LastModified ASC, Version DESC) AS rn FROM Logs WHERE Status = 'Archived') DELETE FROM cte WHERE rn - 优势:子查询结果集可复用,
ROW_NUMBER()确保每次删的是“最早一批”,不会漏删或重复删 - 风险点:CTE 本身不物化,若
DELETE过程中底层数据被并发修改,ROW_NUMBER()结果可能偏移——所以建议在事务内加UPDLOCK, READPAST提示(如FROM Logs WITH (UPDLOCK, READPAST)) - 别省略
WHERE条件:CTE 里没过滤,ROW_NUMBER()会全表计算,性能崩盘
循环脚本里容易忽略的终止条件和延迟控制
手动写 WHILE 循环分批删时,最常踩的坑不是语法,而是“停不下来”或“压垮系统”。
- 必须检查影响行数:
IF @@ROWCOUNT = 0 BREAK,否则空删会无限循环 - 不要用
WAITFOR DELAY '00:00:01'硬等——低峰期可能够用,高峰期并发高时反而积压更多锁;建议用WAITFOR配合@@ROWCOUNT动态调整,比如删完 5000 行后等 100ms,删完 100 行等 10ms -
GO不能出现在循环体内:它会中断批处理上下文,导致变量失效、事务断裂;所有循环逻辑必须在一个批里完成 - 日志备份策略要同步调整:分批删期间,若日志不及时截断,仍可能涨满——确保
BACKUP LOG按需执行,而非依赖自动维护计划
SELECT COUNT(*) 和 DBCC SQLPERF(logspace) 看下当前日志使用率;删的过程中紧盯 sys.dm_exec_requests 里的 wait_type,一旦出现 LOG_RATE_GOVERNOR 或长时间 LCK_M_X,说明节奏太快,得立刻调小 TOP 值或加延时。

















