直接update大表会阻塞交易,因未走索引或扫描过大时锁升级为间隙锁/临键锁甚至整段索引锁,且未提交事务持续持锁,导致下游操作排队;应使用limit+where分批更新,以主键或唯一索引为锚点控制单次锁持有时间。

为什么直接 UPDATE 大表会阻塞交易?
因为 UPDATE 默认加行级锁(InnoDB)或表级锁(MyISAM),但若没走索引、或扫描范围过大,实际会升级为间隙锁/临键锁,甚至锁住整个索引段。更危险的是:事务未提交时,所有被修改的行都持续持锁,下游 SELECT ... FOR UPDATE 或 UPDATE 会排队等待——这就是你看到“订单提交变慢”“库存扣减超时”的根本原因。
用 LIMIT + WHERE 分批更新是基础操作
核心思路是把一个大更新拆成多个小事务,每批只改几百到几千行,控制单次锁持有时间在毫秒级。关键不是“一次改多少”,而是“每次必须有确定的、可复用的进度锚点”:
- 优先用主键或唯一索引列做分页条件,例如:
WHERE id > 100000 AND id ,避免 <code>LIMIT 100000, 1000这种偏移量写法(越往后越慢) - 批量大小建议 500–2000 行:太小导致事务开销占比高;太大仍可能触发锁升级或日志膨胀
- 必须显式加
ORDER BY id(和 WHERE 条件字段一致),否则分批可能漏行或重复 - 示例脚本片段:
UPDATE orders SET status = 'shipped' WHERE id BETWEEN 100001 AND 100500 AND status = 'pending';
WHERE 条件必须走索引,否则分批也白搭
很多人写了 LIMIT 却依然卡住,是因为 WHERE status = 'pending' 没建索引——MySQL 先全表扫出所有 pending 记录,再按 id 截取前 N 行,锁住的仍是全量匹配行。验证方法很简单:
- 执行
EXPLAIN SELECT id FROM orders WHERE status = 'pending' ORDER BY id LIMIT 1000; - 看
type是否为range或ref,key是否显示用了索引名 - 如果
type是ALL,立刻给(status, id)建联合索引(顺序不能错,status 在前才支持该查询)
应用层控制比纯 SQL 更可靠
纯 SQL 脚本难处理失败重试、进度跟踪、并发冲突。真正生产环境推荐用程序驱动:
- 每次只查一批待更新 ID:
SELECT id FROM orders WHERE status = 'pending' ORDER BY id LIMIT 500; - 拿到 ID 列表后,用
UPDATE ... WHERE id IN (1,2,3...)更新(注意 MySQLIN参数上限,默认 65535,需分组) - 更新后立即
COMMIT,再取下一批——这样即使某批失败,也不影响已提交批次 - 加个简单状态表记录最后成功处理的
max_id,断点续传就靠它
最易被忽略的一点:别在凌晨跑全量脚本。哪怕分批,高峰期大量短事务叠加也会推高 Innodb_row_lock_time_avg,监控里看不出单次锁长,但整体 TP99 会悄悄恶化。










