必须拆分百万级UPDATE操作,因其必然导致长事务、行锁膨胀、undo日志激增及主从延迟;应基于主键范围分批(500–1000行/批),确保WHERE走索引、及时COMMIT并加休眠缓解压力。

不能用单条 SQL 更新百万级数据——这不是“怎么避免”的问题,而是“必须拆分”的硬约束。 即使加了索引、开了 autocommit=1,一条 UPDATE 影响百万行,InnoDB 仍会持续持有百万行锁、膨胀 undo log、阻塞 purge 线程,并大概率触发 Lock wait timeout exceeded 或让从库延迟飙升。所谓“避免”,本质是绕开这条语句本身。
为什么单条 UPDATE 百万行必然长事务?
MySQL 的事务生命周期从第一条 DML 开始,到 COMMIT 或 ROLLBACK 结束。单条 UPDATE 扫描并修改百万行,意味着:
- 事务内至少持有百万行的行锁(或升级为间隙锁/表级意向锁)
- undo log 持续增长,可能撑爆
ibdata1或触发磁盘 I/O 瓶颈 -
INFORMATION_SCHEMA.INNODB_TRX中TRX_ROWS_LOCKED轻松破百万,TRX_STARTED时间远超 30 秒 - 主从复制中,从库 replay 该事务时卡在行锁等待,
Seconds_Behind_Master直线拉升
真正可行的分批更新写法(基于主键范围)
核心是放弃 LIMIT + OFFSET,改用主键区间驱动,确保每次查询都走索引、不回表、不跳过数据:
- 确认目标表有连续或较密集的主键(如自增
id),且无大段空洞 - 用
WHERE id BETWEEN @start AND @end替代LIMIT 1000 OFFSET N(后者越往后越慢,且可能漏行) - 每批严格控制在 500–1000 行以内:单行越大,批次越小;
STATEMENTbinlog 格式下更需保守 - 每次
UPDATE后立刻COMMIT,再查ROW_COUNT()判断是否继续 - 批次间加
DO SLEEP(0.05)或SELECT SLEEP(0.05),缓解锁竞争和主从压力
示例逻辑(MySQL 存储过程片段):
SET @start_id = 0; WHILE @start_id <= (SELECT MAX(id) FROM t) DO UPDATE t SET status = 1 WHERE id > @start_id AND id <= @start_id + 1000; SELECT ROW_COUNT() INTO @affected; IF @affected = 0 THEN LEAVE; END IF; SET @start_id = @start_id + 1000; COMMIT; DO SLEEP(0.05); END WHILE;
WHERE 条件没走索引?分批也白搭
这是最常被忽略的致命点:哪怕你写了 WHERE id BETWEEN ...,如果 id 列没索引,或类型不匹配(比如 id 是 BIGINT 却传字符串),执行计划仍是 type: ALL,全表扫描 + 全表加锁,分批毫无意义。
- 运行
EXPLAIN UPDATE ...确认key列显示实际使用的索引,rows接近你设定的批次大小 - 避免隐式转换:
WHERE id = '123'在id为数字类型时会失效索引 - 若真无合适索引,优先建索引;若无法建(如中间件分库分表),改用主键子查询方式定位起点
事务隔离级别与锁行为的关系
默认 REPEATABLE READ 下,范围条件(如 BETWEEN)会加间隙锁(Gap Lock),扩大锁定范围,增加死锁概率。对非强一致性场景,可临时降级:
- 会话级执行
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED - 此时
UPDATE只锁实际命中行,不锁间隙,显著降低冲突 - 但需确认业务能接受“不可重复读”——例如后台任务、状态批量翻转类操作通常可以
- 切勿全局修改
transaction_isolation,防止账务等核心链路出错
真正难的不是写出分批逻辑,而是确保每次 WHERE 都走索引、每次 COMMIT 都及时、每次锁都只落在必要行上。漏掉任意一环,百万级更新就又退回到“像锁了整张表”的状态。


















