结论:row_number()分片删除通过编号+索引排序实现可复现、可跳过、可并行的精准分批删除,避免全表扫描和数据变动导致的漏删/重复删,但必须用索引字段排序且只选关键列,否则反而更慢或死锁。

直接说结论:用 ROW_NUMBER() 分片删除,本质是“先编号、再分批筛、最后删”,不是为了省事,而是避免锁表、阻塞业务、触发事务日志爆炸——但若不控制好分区和索引,反而会让删除更慢甚至死锁。
为什么不能直接 DELETE TOP (N)?
看起来简单:DELETE TOP (5000) FROM 表 WHERE 条件。但问题在:每次执行都得重新扫描全表找匹配行,没有稳定锚点;如果条件字段没索引,5000 行可能扫几百万行;高并发下容易出现重复删或漏删(因中间数据变动);且 TOP 不保证顺序,删的可能是任意 5000 行,不利于按时间/状态等逻辑清理。
用 ROW_NUMBER() 分片的核心,是把“删哪几行”变成一个可复现、可跳过、可并行的编号区间。
ROW_NUMBER() 分片删除的正确写法
关键不是套个子查询,而是让编号计算能走索引、不拖慢扫描,并且删除语句能命中目标行。
- 必须在
OVER(ORDER BY ...)中使用**已建索引的字段**(如主键id或时间字段created_time),否则排序会触发大量磁盘排序(Sort算子),5000 行就可能卡住 - 不要在子查询里
SELECT *—— 只选id(或聚簇索引键)和用于排序的字段,减少中间结果集体积 - 删除时用
IN或JOIN,别用WHERE rowindex BETWEEN ...套两层子查询,SQL Server 很难优化这种嵌套
推荐写法(以按主键分批删为例):
WITH batch AS (
SELECT id
FROM (
SELECT id, ROW_NUMBER() OVER (ORDER BY id) AS rn
FROM target_table
WHERE status = 0 AND created_time
<h3>常见翻车现场和绕过方式</h3>
<p>实际跑起来常遇到三类问题:</p>
-
删着删着变越来越慢:因为每批都重算
ROW_NUMBER(),而 WHERE 条件未随批次收缩(比如还是查全表满足status=0)。解决办法是加游标或用上一批最大id作为下一批起点:WHERE id > @last_id AND status = 0 -
删完发现漏了或重复:当
ORDER BY字段不唯一(比如多个记录created_time相同),ROW_NUMBER()排序不稳定,两次执行编号可能错位。必须补一个唯一字段兜底,例如:ORDER BY created_time, id -
事务日志暴涨:5000 行一批仍可能撑爆日志(尤其简单恢复模式)。建议显式控制事务:
BEGIN TRAN; DELETE ...; COMMIT TRAN;,并监控log_reuse_wait_desc
比 ROW_NUMBER() 更稳的替代方案
如果 SQL Server 版本 ≥ 2012,优先用 OFFSET / FETCH 配合循环删除——它底层做了优化,不依赖窗口函数,执行计划更干净:
DECLARE @offset INT = 0, @batch INT = 5000;
WHILE 1=1
BEGIN
DELETE TOP (@batch) FROM target_table
WHERE id IN (
SELECT id FROM target_table
WHERE status = 0
ORDER BY id
OFFSET @offset ROWS FETCH NEXT @batch ROWS ONLY
);
IF @@ROWCOUNT
<p>注意:这里 <code>ORDER BY id</code> 仍需确保有索引,且 <code>DELETE TOP</code> 的子查询必须带 <code>ORDER BY</code>,否则报错。</p>
<p>真正容易被忽略的是:分片删除不是纯 SQL 技巧问题,而是和事务隔离级别、索引维护、自动增长日志文件策略强相关。没做预估就在线上跑,可能比不删还危险。</p>










