无索引条件的update不会主动锁全表,而是因全表扫描并逐行加x锁与间隙锁,导致锁行数≈表总行数,效果等同锁全表;根本原因是innodb行锁基于索引记录,无索引则无法精确定位,只能保守地锁所有扫描行。

无索引条件的 UPDATE 语句不会“主动锁全表”,但会因全表扫描 + 逐行加锁,最终效果等同于锁全表——这是 InnoDB 行锁机制的必然结果,不是配置或版本问题。
为什么没索引就等于扫全表并锁住所有行
InnoDB 的行锁加在索引记录上,不是加在“数据行”本身。当 WHERE 条件字段(如 status、city)没有可用索引时:
- 优化器只能选择
type = ALL全表扫描,遍历聚簇索引(即整张表的主键顺序) - 每扫描到一条聚簇索引记录,InnoDB 就尝试加 X 锁(记录锁),并在可重复读(RR)隔离级别下顺带加 Gap 锁
- 哪怕最终只更新 1 行,也得先锁住全部扫描过的行及其间隙——锁数量 ≈ 表总行数
-
SHOW ENGINE INNODB STATUS中的trx_rows_locked接近表总行数,就是典型信号
EXPLAIN 能直接验证是否真锁了全表
MySQL 8.0.19+ 支持对 UPDATE 直接执行 EXPLAIN,关键看三列:
-
type是ALL或index→ 全表或全索引扫描,风险已存在 -
key为NULL→ 没命中任何索引(常见于隐式类型转换,如user_id = '123'对INT字段) -
rows接近表总行数 → 实际扫描范围过大,锁必然密集 - 函数包裹字段也会失效:
WHERE DATE(created_at) = '2026-01-01'必须改写为created_at BETWEEN ...
建索引不等于解决问题,顺序和匹配度才是关键
索引建错,和没建一样。真正起作用的是「索引能否把扫描行数压到最小」:
- 单等值条件(
WHERE status = ?)→ 建单列索引,但要注意区分度:若 95% 都是'pending',即使有索引,仍可能锁几十万行 - 多条件组合(
WHERE status = ? AND created_at > ?)→ 必须建联合索引,且顺序为(status, created_at);反过来就基本无效 -
FORCE INDEX不能绕过锁范围问题:它只影响访问路径,不减少实际扫描和加锁的行数 - 低区分度字段(如
is_deleted)单独建索引意义不大,优先考虑覆盖查询的联合索引
分批更新不能靠 LIMIT OFFSET,必须用主键游标
UPDATE ... LIMIT 5000 看似安全,但 WHERE 仍可能全表扫,锁照样加满:
- 正确做法是用有索引的单调字段(如
id)做范围切分:WHERE id > 100000 AND id - 每次执行后取
MAX(id)作为下一批起点,避免漏更或重复 - 每批必须独立事务,执行完立刻
COMMIT,锁才真正释放 - 批次大小建议 1000–5000 行:太小事务开销大;太大锁持有时间长,仍可能阻塞其他业务
最容易被忽略的一点:加完索引或改写分批逻辑后,必须在从库或备份库完整跑一遍,确认最终更新总数和业务含义完全一致——因为中间若有新插入或状态变更,仅靠预估页数或偏移量极易出错。











