单事务批量更新百万级数据必然锁表,应改用主键递进分批(SQL Server)、变量游标模拟分片(MySQL)或BULK COLLECT+FORALL(Oracle),并避免事务内混入耗时操作。

直接用单事务批量更新百万级数据,基本等于主动锁表——不是“可能锁”,而是“必然锁住其他业务”。关键不在UPDATE语句本身,而在事务粒度、索引覆盖和推进方式。
SQL Server:别用OFFSET/FETCH,改用TOP+主键推进
OFFSET/FETCH在10万行后性能断崖下跌,且每次都要跳过前N行,CPU和IO双爆。真正稳的写法是靠主键递进分批:
-
DECLARE @min_id BIGINT = (SELECT MIN(id) FROM orders WHERE status = 'pending')—— 起点必须动态查,不能硬编码 - UPDATE语句必须带
ORDER BY id,否则TOP (5000)行为不可预测 - 每批执行完立刻刷新起点:
SELECT @min_id = MIN(id) FROM orders WHERE status = 'pending' AND id > @min_id - 加
IF @@ROWCOUNT = 0 BREAK,防止空结果集导致无限循环 -
status和id必须建复合索引,否则每次WHERE都全表扫描
MySQL:用变量游标模拟分批,绕过UPDATE+LIMIT限制
MySQL不允许UPDATE ... JOIN ... LIMIT,但可以用变量构造逻辑游标实现等效分片:
- 每次执行前必须重置变量:
SET @row_index := -1,否则第二次运行会漏数据 - 子查询里
ORDER BY id不可省,否则@row_index分配顺序不保证 - 典型写法:
UPDATE orders SET status = 'processed' WHERE id IN (SELECT id FROM (SELECT id, @row_index := @row_index + 1 AS row_num FROM orders WHERE status = 'pending' ORDER BY id LIMIT 5000) AS t) - 并发场景下可能漏行或重复,建议加
SELECT ... FOR UPDATE预占,或由应用层用分布式锁协调 - WHERE条件字段(如
status)必须有索引,且不能是表达式,否则ORDER BY触发filesort
Oracle:BULK COLLECT+FORALL才是真批量
WHILE+SELECT INTO在百万级下必然失败:全量加载撑爆PGA、回滚段暴涨、每批都全表排序。必须用原生批量机制:
-
LIMIT推荐值固定为100~500,Oracle 19c实测LIMIT 500吞吐与内存占用最平衡 - 必须搭配
FORALL使用,否则只是“伪批量”,上下文切换开销照旧 - 游标里加
/*+ INDEX(a idx_status_time) */强制走索引,WHERE条件字段不能包函数(如TRUNC(create_time)) - 每次FETCH后检查
v_batch.COUNT = 0再退出,不能只靠%NOTFOUND - 每批处理完要
COMMIT,但别无条件commit——建议每1000行一次,避免I/O过载
所有数据库共通陷阱:事务内混入耗时操作
存储过程里一旦出现xp_cmdshell、sp_OACreate、HTTP调用(如sp_http_get)或大量字符串拼接/加密解密,事务就会被挂起,锁一直不释放。这类逻辑必须移出事务边界。
最容易被忽略的是:哪怕你写了循环,只要没显式COMMIT,整个过程仍在一个长事务里。10万行更新卡住,问题不在SQL慢,而在锁住了其他业务——这个边界感,比语法更重要。

















