分批更新加索引覆盖可解决90%的大表更新锁超时问题:where条件必须命中索引,update语句应尽量实现索引覆盖以减少回表和锁行数,batch_size需据实时性能动态调整。

大表更新卡住、报 Lock wait timeout exceeded 或频繁死锁,不是数据量太大,而是锁没放对、没放及时。 直接上分批 + 索引覆盖,90% 的场景能压到毫秒级响应。
UPDATE 为什么一跑就锁表?
InnoDB 行锁只在 WHERE 条件命中索引时生效;没索引或索引失效(比如 WHERE status = 'pending' AND create_time 但 <code>status 没单独建索引),MySQL 就会扫描全表,对每行加记录锁——效果等同于表锁。
- 用
EXPLAIN UPDATE ...(MySQL 8.0.22+ 支持)或先改写成EXPLAIN SELECT ...查key列是否非NULL、rows是否接近表总行数 - 避免在 WHERE 中用函数(
DATE(create_time))、隐式转换(id = '123'但 id 是 INT)、OR、LIKE '%abc' - 高频更新字段(如
status、updated_at)必须放在联合索引前列,例如INDEX idx_status_updated (status, updated_at, id)
分批更新必须带 WHERE 条件和 LIMIT,不能只靠 ORDER BY
单纯 UPDATE ... ORDER BY id LIMIT 1000 在高并发下可能重复更新同一行(因其他事务已改过 id 对应的行状态),必须锁定范围起点。
- 安全写法是:每次取上一批最大
id,再查下一批 ——UPDATE t SET status='done' WHERE id > 10000 AND status='pending' ORDER BY id LIMIT 1000 - 循环中必须用
ROW_COUNT()判断是否还有数据,避免空跑;每次执行后立刻COMMIT - 不要用
WHILE ... DO ... END WHILE包裹整个逻辑,MySQL 存储过程里变量作用域和错误处理不透明,优先用应用层控制(Python/Java 调用)
索引覆盖能绕过聚簇索引回表,减少锁行数
UPDATE 本质是“查 + 改”,如果 WHERE 和 SET 字段都能被同一个索引覆盖,InnoDB 就不必访问主键 B+ 树,自然少锁几行。
- 例如要执行
UPDATE orders SET status='shipped', updated_at=NOW() WHERE user_id=123 AND status='paid',建联合索引INDEX idx_user_status (user_id, status, updated_at)——updated_at放最后,既满足 WHERE 又覆盖 SET - 注意:
TEXT/BLOB字段不能建在索引里;如果 SET 的字段太大(如json_data),索引覆盖收益归零,此时优先保证 WHERE 索引高效 - 用
SELECT COUNT(*) FROM t WHERE ...配合EXPLAIN验证索引是否真被用于过滤,而不是仅用于排序
真正容易被忽略的点是:分批的 batch_size 不是越大越好。1000 是经验值,但如果单次 UPDATE 平均耗时超过 500ms,说明 I/O 或 CPU 已成瓶颈,该调小到 200–500;反之若 consistently sub-50ms,可试探拉到 2000。锁冲突不是静态问题,它随实时负载浮动——没有一劳永逸的配置,只有持续观测 INFORMATION_SCHEMA.INNODB_TRX 和慢日志里的 Lock_time。











