直接update会锁表,因where无索引触发全表扫描并加锁所有聚簇索引记录+间隙;即使有索引,大事务也会导致undo log膨胀、mvcc版本链拉长及主从延迟。

不能靠单条 UPDATE 语句硬改,否则表会锁死几十分钟甚至更久;必须分批、绕开全表扫描、避开长事务阻塞。
为什么直接 UPDATE 会锁表?
MySQL 在执行 UPDATE 时,会对匹配到的每一行加行锁(InnoDB 默认),但如果 WHERE 条件没走索引,就会退化为全表扫描 + 全表加锁;哪怕有索引,一次性更新千万行也会让事务日志暴涨、undo log 堆积、主从延迟飙升。更糟的是,UPDATE ... WHERE status = 0 这类语句在无索引或低选择率字段上,极易触发表级等待。
- 没索引的
WHERE字段 → 全表扫描 → 所有聚簇索引页被锁定 - 单事务更新太多行 → undo log 膨胀 → MVCC 版本链拉长 → 其他查询卡在一致性读
- binlog 写入大事务 → 主从同步卡住,延迟跳到小时级
用 WHERE id BETWEEN ? AND ? 分批次更新
这是最可控、无需额外工具、兼容所有 MySQL 版本的做法。核心是利用主键或高基数索引字段做切片,每次只操作几万行,让锁持有时间控制在秒级内。
- 先确认表有自增主键
id或带索引的有序字段(如create_time) - 每次取一段连续 ID:例如
WHERE id BETWEEN 1000001 AND 1100000 - 每批
UPDATE后加SLEEP(0.1)(应用层控制),避免 I/O 打满 - 用循环脚本或存储过程驱动,记录最后处理的
id防重跑
示例片段(存储过程逻辑):
DECLARE low_id BIGINT DEFAULT 0;
DECLARE high_id BIGINT;
DECLARE batch_size INT DEFAULT 50000;
WHILE low_id low_id AND status = 'pending'
LIMIT 1;
IF high_id IS NULL THEN LEAVE; END IF;
UPDATE orders
SET status = 'processed'
WHERE id BETWEEN low_id AND high_id
AND status = 'pending';
SET low_id = high_id + 1;
END WHILE;
用临时表 + JOIN 替代全表 WHERE
当无法依赖主键分片(比如要按非索引字段批量更新),可用临时表预筛选 ID,再通过 JOIN 更新——它比子查询更稳定,且 MySQL 优化器通常能走 eq_ref 访问类型,避免重复扫描原表。
- 临时表必须建在
INFORMATION_SCHEMA以外的库,且用CREATE TEMPORARY TABLE(会话级自动清理) - 临时表字段只需存
id,不要冗余其他列 -
UPDATE ... JOIN的写法比UPDATE ... WHERE id IN (SELECT ...)更安全,后者在旧版本可能被转成派生表导致全表扫描
示例:
CREATE TEMPORARY TABLE tmp_ids (id BIGINT PRIMARY KEY); INSERT INTO tmp_ids SELECT id FROM orders WHERE status = 'error' LIMIT 10000; UPDATE orders JOIN tmp_ids ON orders.id = tmp_ids.id SET status = 'retried';
别忽略 binlog 和从库压力
即使你把更新拆得很细,如果业务开启了 binlog_format = STATEMENT,每个 UPDATE 仍会以原始 SQL 形式写入 binlog,从库回放时可能因并发度低而堆积。千万级批量修改前务必确认:
- 主库
binlog_format是ROW(推荐),避免从库解析失败 - 从库已开启
slave_parallel_workers > 0,并确认slave_parallel_type = LOGICAL_CLOCK - 监控
Seconds_Behind_Master和Innodb_row_lock_waits,发现异常立即暂停批次
真正容易被忽略的点是:分批更新不是“只要不报错就算成功”,而是每批提交后都要验证数据一致性(比如校验 COUNT(*) 或抽样 SELECT),否则中间出错可能导致部分数据漏更新,而日志里根本看不出。











