分批提交事务是避免数据库崩溃的底线操作,因单次大事务会同时压垮锁、日志和内存:每行加next-key lock导致百万行锁堆积,undo/redo日志爆满,全表扫描触发锁升级;必须每批1000–5000行、基于索引推进(如where id > @last_id)、显式commit释放资源,禁用offset,依赖row_count()终止,且确保复合索引(如status,id)覆盖查询条件。

分批提交事务不是“更优雅”的写法,而是避免数据库崩溃的底线操作。一次性更新百万行,大概率触发锁升级、日志爆满、连接超时,甚至拖垮整个实例。
为什么单次大事务会卡死数据库
核心是锁、日志、内存三者同时被压垮:
- InnoDB 对每行加 next-key lock,100 万行 = 持有 100 万把锁,直到 COMMIT;其他事务访问任一被锁行都会阻塞
- SQL Server / PostgreSQL 的事务日志必须记录所有旧值(undo)和新值(redo),单事务写满日志文件直接报
transaction log is full - MySQL 的
innodb_log_file_size若太小,频繁刷盘导致 I/O 瓶颈;PostgreSQL 的pg_xactWAL 段可能撑爆磁盘 - 执行计划里出现
type=ALL(全表扫描)时,一次性 UPDATE 实际要遍历整张表,哪怕只改其中 1% 的行
分批提交如何真正缓解压力
关键不在“分”,而在“每批独立 COMMIT”——释放锁、清空 undo、截断 redo、归还内存:
- 每批控制在
1000–5000行:足够小以避免单批超时(如 >30 秒),又足够大以减少网络往返开销 - 必须用索引字段推进,例如
WHERE id > @last_id ORDER BY id LIMIT 5000,全程走index seek,不扫无关数据 - 每批后显式
COMMIT,不是等循环结束再统一提交;否则仍是一个长事务,只是拆成了多个语句 - 别用
OFFSET或LIMIT N OFFSET M:第 100 批要跳过 50 万行,引擎就得重复扫描丢弃前 50 万行,性能断崖下跌
容易被忽略的终止条件和索引陷阱
写对循环逻辑比写对 UPDATE 语句更难:
- 终止必须靠
@@ROWCOUNT == 0(SQL Server)或ROW_COUNT() == 0(MySQL),不能靠预估总行数循环固定次数——因为 WHERE 条件随更新动态变化(比如 status 改了,后续批次就查不到原条件的数据) -
WHERE status = 'pending' AND id BETWEEN ? AND ?这类写法,若status列没索引,InnoDB 仍会全表扫描所有id区间内的行来过滤status,锁住的远不止目标行 - 真正有效的索引是复合索引,顺序按选择性从高到低,例如
(status, id);单建status索引效果有限,因该字段基数低
最常被跳过的一步:上线前没在从库上用 EXPLAIN FORMAT=TREE 或 EXPLAIN ANALYZE 验证执行计划是否真的走了索引。看起来在跑,其实每批都在全表扫描。











