根本原因是InnoDB行锁只能加在索引记录上;无索引时需全表扫描聚簇索引,对每条扫描记录加X锁+间隙锁,等效锁表。

更新无索引列会锁死整张表,根本原因不是 MySQL “要锁表”,而是 InnoDB 行锁只能加在索引记录上;没索引,就只能扫聚簇索引(即整张表),并对每条扫描到的记录加 X 锁 + 间隙锁——效果等同于锁表。
为什么没索引就等于锁全表
InnoDB 的行锁对象是「索引记录」,不是“数据行”。当 WHERE 字段没索引时,优化器被迫走 type = ALL 全表扫描,引擎层就得逐条判断并加锁:
- 每扫描到一条聚簇索引记录(也就是主键行),就加一个记录锁(
REC_NOT_GAP) - 在可重复读(
RR)隔离级别下,还会顺带把相邻间隙也锁住(GAP) - 哪怕最终只有一行匹配,所有被扫描的主键值及其间隙都会被锁死
所以不是“锁升级”,而是“锁得太多”——扫描多少行,就锁多少行+间隙。
怎么一眼看出 UPDATE 正在锁全表
别靠猜,直接用 EXPLAIN 看执行计划,盯这三个字段:
-
type是ALL或index→ 全表或全索引扫描,基本没走有效索引 -
key显示NULL→ 没命中任何索引 -
rows接近表总行数(比如表有 80 万行,rows显示 79 万)→ 实际扫描范围失控
典型失效场景:WHERE DATE(created_at) = '2026-09-01'、WHERE user_id = '123'(user_id 是 INT)、WHERE status = ? 但 status 列没有索引。
建什么索引才真正管用
不是“加个索引”就行,得匹配查询模式。高频更新的 WHERE 条件必须覆盖,且顺序不能错:
- 单列等值条件 → 直接建单列索引:
ALTER TABLE orders ADD INDEX idx_status (status) - 多列组合条件(如
WHERE status = ? AND created_at > ?)→ 必须建联合索引,且顺序为“等值在前、范围在后”:ALTER TABLE orders ADD INDEX idx_status_created (status, created_at) - 别建反了:
(created_at, status)对status = ? AND created_at > ?基本无效
如果 status 值分布极不均匀(比如 95% 都是 'pending'),即使有索引,也会锁住几十万行——此时应考虑先查 ID 列表再分批更新。
强制索引(FORCE INDEX)有用吗
多数情况下没用。因为 FORCE INDEX 只影响访问路径选择,不改变锁行为本质:
- 如果强制的索引本身不适合该查询(比如单列
idx_created_at用于WHERE status = ? AND created_at > ?),优化器仍可能拒绝使用 - 更关键的是,
FORCE INDEX对全表扫描场景下的加锁范围毫无约束力——扫多少行,就锁多少行+间隙 - 验证是否真用了索引,得看
EXPLAIN FORMAT=JSON中的used_key和key_length
真正有效的解法是建覆盖查询模式的联合索引,而不是依赖提示。
最容易被忽略的一点:锁本身是行级的,但事务不提交,锁就一直挂着。别人想改同一行就得排队,人一多,看起来就像“表被锁死”。这不是锁升级,是锁被占着不放。

















