游标分段删除比DELETE TOP更难控制,因其无事务边界、依赖快照易定位失效,须用KEYSET/SCROLL游标配合显式事务与TRY...CATCH,MySQL则不支持WHERE CURRENT OF,应改用LIMIT循环。

游标分段删除为什么比 DELETE TOP 更难控制
因为游标本身不带事务边界控制,FETCH 和 DELETE 之间若没显式事务包裹,极易出现重复删或漏删;更麻烦的是,游标基于结果集快照,而归档表常被其他进程写入,导致 WHERE CURRENT OF 定位失效或报错 Cursor is not positioned on a row。
- 必须用
SCROLL或KEYSET游标(不能用FORWARD_ONLY),否则无法在循环中安全定位当前行 - 每次
FETCH后立刻执行DELETE WHERE CURRENT OF,中间不能穿插任何影响游标位置的操作(比如再开一个游标、调用存储过程) - 强烈建议搭配
TRY...CATCH+ 显式BEGIN TRAN / COMMIT / ROLLBACK,单次失败不影响整体进度 - 别依赖游标计数做“删满10万条就停”,要以实际
@@FETCH_STATUS = 0为准——有些行可能已被其他事务锁住或删除
SQL Server 中用 KEYSET 游标实现稳定分段删
KEYSET 游标会把键值缓存到临时工作表,后续 FETCH 不受底层数据变更干扰,适合归档表这种高并发低更新场景。但注意:它只支持可唯一标识的列(如主键或含唯一索引的组合)。
DECLARE @batch_size INT = 5000; DECLARE @deleted_count INT = 0; <p>DECLARE archive_cursor CURSOR KEYSET FOR SELECT id FROM dbo.archive_log WHERE create_time < '2022-01-01' ORDER BY id; -- 必须有确定排序,否则 KEYSET 行为不可控</p><p>OPEN archive_cursor; FETCH NEXT FROM archive_cursor;</p><p>WHILE @@FETCH_STATUS = 0 AND @deleted_count < @batch_size BEGIN BEGIN TRY BEGIN TRAN; DELETE FROM dbo.archive_log WHERE CURRENT OF archive_cursor; COMMIT; SET @deleted_count += 1; END TRY BEGIN CATCH ROLLBACK; -- 记录错误但继续下一行,避免卡死 PRINT 'Failed to delete id=' + CAST(@@ERROR AS VARCHAR(10)); END CATCH</p><p>FETCH NEXT FROM archive_cursor; END;</p><p>CLOSE archive_cursor; DEALLOCATE archive_cursor;
关键点:ORDER BY id 不可省,否则 KEYSET 可能跳行;@@ERROR 在 CATCH 块里才有效,直接写在外面会取不到值。
MySQL 不支持 WHERE CURRENT OF,得换路子
MySQL 8.0+ 的游标是只读且无定位删除能力的,强行用 DECLARE ... CURSOR + FETCH 再拼 DELETE WHERE id = ? 效率极低,还容易因并发更新导致误删。真要分段删,老实用 LIMIT + 循环。
- 用
SELECT id FROM archive_log WHERE create_time 拿一批 ID - 把结果塞进临时表或内存变量(MySQL 8.0+ 支持
CTE+ROW_NUMBER()分页) - 再
DELETE FROM archive_log WHERE id IN (SELECT id FROM temp_batch) - 务必加
ORDER BY id和FOR UPDATE SKIP LOCKED(如果用SELECT ... FOR UPDATE方式取 ID)防止重复扫描
示例片段(MySQL 8.0):
SET @row_number := 0;
DELETE t1 FROM archive_log t1
INNER JOIN (
SELECT id FROM (
SELECT id, (@row_number := @row_number + 1) AS rn
FROM archive_log
WHERE create_time < '2022-01-01'
ORDER BY id
) AS ranked
WHERE rn <= 5000
) t2 ON t1.id = t2.id;真正该警惕的不是语法,而是锁和日志暴涨
哪怕游标逻辑完全正确,一次删 5000 行也可能让事务日志撑爆,或长时间持有表锁阻塞业务写入。尤其在 SQL Server 上,DELETE 默认每行记一条日志,不如 TRUNCATE 轻量——但 TRUNCATE 不能带 WHERE。
- SQL Server:考虑改用分区表,直接
SWITCH分区出去再DROP,比游标快十倍且不写日志 - PostgreSQL:用
DELETE ... LIMIT 5000循环,配合VACUUM策略,比游标简单可靠 - 所有数据库:删前确认归档表有没有外键引用、触发器、复制订阅——这些会让游标中途报错且难以捕获
游标不是银弹,它只是“可控”而非“高效”。当发现 FETCH 越来越慢、tempdb 空间告警、或者 DBA 开始盯你 session 的时候,就得停下来想:是不是该切到基于时间范围的批量 DELETE + 索引优化了。

















