大表UPDATE锁全表主因是未走索引导致全表扫描并加大量行锁;WHERE条件无索引、区分度低、复合索引顺序错、隐式类型转换均会失效;分批更新须用主键范围而非LIMIT OFFSET。

大表 UPDATE 锁全表,根本不是语句“写错了”,而是 MySQL 在执行时被迫扫全表 + 持有大量行锁 + 事务不提交,最终等效于锁表。只要没走索引、单次改太多、或事务卡住,就大概率触发。
WHERE 没走索引:锁表的头号原因
哪怕你写了 WHERE status = 'pending',只要 status 列没有索引,或区分度极低(比如 95% 都是 pending),优化器就会放弃走索引,直接全表扫描。InnoDB 会对每一条扫描到的行加 next-key lock——扫 1000 万行,就锁 1000 万行,其他事务一碰就等。
- 用
EXPLAIN FORMAT=TREE看type字段:要是ALL或index,基本就是全表/全索引扫描 -
key为空,或显示的不是你建的那个索引名,说明没命中 - 复合索引顺序必须匹配查询条件:比如
(deleted, create_time)能用上deleted = 0,但(create_time, deleted)就不一定 - 隐式类型转换会失效:
user_id是BIGINT,却传字符串'123',索引直接跳过
MySQL 分批 UPDATE 必须用主键游标,不能靠 LIMIT OFFSET
UPDATE ... LIMIT 5000 看似能控制数量,但它不解决扫描问题:WHERE 条件仍可能全表扫,只是最后只改前 5000 行。真正安全的分片,得靠单调递增字段(如 id)做范围切分,每次只扫一小段。
- 正确写法:
UPDATE orders SET status = 'shipped' WHERE id > 100000 AND id - 下一批起点必须是上一批的
MAX(id),不是靠SELECT COUNT算总页数 - 禁用
OFFSET:并发更新时数据变动会导致偏移漂移,漏更或重复 - 每批单独事务,执行完立刻
COMMIT,锁才真正释放
SQL Server 的 TOP 分批更简单,但必须配 ORDER BY 和事务控制
SQL Server 支持 UPDATE TOP(n),语法更直白,但容易忽略两个关键点:一是没 ORDER BY 会导致同一批重复更新或漏更;二是没显式事务包裹,每条 TOP 都是独立事务,反而增加开销。
- 必须写成:
BEGIN TRANSACTION; UPDATE TOP (1000) t SET flag = 1 FROM logs t WHERE t.flag = 0 ORDER BY id; COMMIT; - 终止条件用
IF @@ROWCOUNT = 0 BREAK,别预估总行数 - 避免
WHERE id IN (SELECT ...):子查询可能缓存在 tempdb,几十万 ID 就吃光内存,还扩大锁范围 - 锁升级阈值默认 5000 行,所以批次建议严格控制在 1000–5000
最容易被忽略的一点:分批脚本上线前,必须在从库或备份库完整跑一遍,确认最终更新总数和业务逻辑完全对得上——因为 WHERE 条件里混了状态字段(比如 status = 'pending')时,中间若有新插入或并发修改,游标推进就可能错位。

















