UPDATE 没加索引会锁全表吗?

会——但准确说法不是「上了表锁」,而是 RR 隔离级别下,InnoDB 对全表扫描经过的每一条记录都加了 Next-Key Lock,效果等同锁全表。这是行锁体系里最危险的一种事故,本文还原事故现场、拆解根因、给出完整防御体系。

locks 系列第三篇 · 前两篇讲「锁的种类」与「加锁过程」,本篇聚焦一条经典事故链。
根因:行锁加在索引上 RR 下扫过即锁 RC 有半一致读豁免 治本:给 WHERE 建索引
1
问题拆解
按提纲四步走,把一个事故题讲成一次复盘。

① 会锁全表吗?

先给结论再抠字眼——「锁全表」三个字里藏着两个常见误解:把它当成「表锁」,以及以为「连 SELECT 也读不了」。

② 为什么会发生?

从「行锁加在索引上」这条第一性原理出发,推演出无索引 → 全表扫描 → 扫过即锁的完整链条。

③ 如何避免?

一套事前 / 事中 / 事后的防御体系:索引、EXPLAIN、安全更新模式、慢日志、SQL 审核、隔离级别选型。

前置知识(前两篇已讲,这里一句话带过):InnoDB 行锁不是锁「数据行」,而是锁「索引记录」;RR 下默认加 Next-Key Lock(记录 + 前面的间隙)。这是理解本篇事故的全部前置。
2
UPDATE 没加索引,会锁全表吗?
会,但要抠准「锁全表」的准确含义——三个误解先拆掉。
常见说法对不对准确表述
「UPDATE 没索引会加表锁」 不准确 InnoDB 加的仍然是行级 Next-Key Lock,只不过被加锁的是每一条记录——是「很多把行锁」,不是「一把表锁」。真正意义上的表锁(LOCK TABLES / MyISAM)是另一回事。
「锁全表 = SELECT 也读不了」 不对 普通 SELECT 是快照读(MVCC),完全不受行锁影响,照常能读。被阻塞的只有其它写操作(UPDATE / DELETE / INSERT / 当前读)。
「RR 和 RC 下行为一样」 不一样 RR:全表所有记录 + 间隙都被 Next-Key Lock 锁死,直到事务提交。RC:不满足 WHERE 条件的行加锁后立即释放(semi-consistent read),最终只锁住命中的行。详见第 ⑤ 节。
所以准确的结论是:在 RR 隔离级别(MySQL 默认)下,一条没走索引的 UPDATE 会让 InnoDB 对全表所有索引记录逐条加上 Next-Key Lock——虽然不是表锁,但阻塞效果等同锁全表:在事务提交前,这张表的所有写操作都会被卡住,只有快照读幸免。
3
事故现场还原:一个实验看懂危害
两个会话、一条 SQL,亲眼看「改一行的人堵死全表的人」。
-- 准备:name 字段没有任何索引
CREATE TABLE t_user (
  id   INT PRIMARY KEY,
  name VARCHAR(50)      -- 没建索引!
) ENGINE=InnoDB;

-- ════ 会话 A:执行一条没走索引的 UPDATE ════
BEGIN;
UPDATE t_user SET name = 'X' WHERE name = '张三';

-- ════ 会话 B:更新一条完全不相干的行 ════
BEGIN;
UPDATE t_user SET name = 'Y' WHERE id = 25;
-- ↑ 卡住!!明明只改 id=25,和 张三 毫无关系,
--   却要等 A 提交(或 50 秒锁超时)才能继续。

-- ════ 会话 C:普通 SELECT,不受影响 ════
SELECT * FROM t_user WHERE id = 25;
-- ↑ 正常返回。快照读走 MVCC,不需要锁。
图 1 · 事故时序:A 全表加锁,B 无辜躺枪,C 快照读幸免
t = 时间轴 ↓ 会话 A 会话 B 会话 C UPDATE name='张三' 全表逐条加 Next-Key Lock UPDATE id=25 等锁…… 等 50s 超时报错 SELECT id=25 秒回 ✓ 快照读无锁 持有的锁 并发一高:所有写请求全部排队 → 连接池耗尽 → 上游服务雪崩
雪崩是怎么滚起来的:一条慢 UPDATE 锁全表 → 后续写请求全部等锁 → 每个等锁请求占用一个数据库连接 → 连接池耗尽 → 新请求连库都连不上 → 应用线程同步阻塞 → 上游服务线程池打满 → 整个调用链雪崩。一次「WHERE 条件没走索引」可以掀翻一组服务——这就是事故的本质。
4
为什么会发生这种事故?
从第一性原理推:三个环节缺一不可。
图 2 · 事故根因链:每一环都是「设计如此」,连起来就是灾难
① 行锁加在索引上 锁的是索引记录 不是数据行本身 ② 无索引 → 全表扫描 定位不到目标行 只能从头扫到尾 ③ 扫过即锁 + 防幻读 RR 下扫到的记录 都要加 Next-Key Lock ④ 等效锁全表 直到事务提交 所有写阻塞 → 雪崩

对比看透本质:同一张表,索引决定锁的范围

同样的 UPDATE ... WHERE name='张三',给 name 建上索引之后,锁的范围从「全表」缩到「一行 + 一个间隙」:

图 3 · 有索引 vs 无索引:锁范围天差地别(索引值分布 1,5,10,15,20,25)
UPDATE t_user SET name='X' WHERE name='张三' 有 name 索引(RR) 1 5 10 张三 下一行 只锁 (上一行, 张三] + (张三, 下一行) ≈ 两把锁 无 name 索引(RR) 1 5 10 15 20 25 (-∞,1] (1,5] (5,10] (10,15] (15,20] (20,25] (25,+∞] 全部 Next-Key Lock —— N 行表就是 N 组锁
我的理解——这不是 MySQL 的 bug,而是「职责所在」:InnoDB 锁索引记录,是因为索引才是「定位一行」的唯一手段。没有索引时,MySQL 根本无法知道「哪些行会满足 WHERE」,为了不出错(防幻读、防漏锁),只能宁可错杀不可放过——把扫过的都锁住。所以事故的锅不在 InnoDB,而在「让 InnoDB 盲扫的 SQL」。理解了这一点,防御方案就自然浮出来了:要么给它索引,要么拦住这种 SQL 上线。
5
RC 隔离级别为什么不会锁全表?
半一致读(semi-consistent read)是 RC 的隐藏福利。

同样一条无索引 UPDATE,在 RC 下只锁「最终满足 WHERE 的行」。原因是 RC 下 InnoDB 采用了半一致读策略:

图 4 · RC 的半一致读:先「试读」不锁,确定命中才真正加锁
UPDATE ... WHERE name='张三'(无索引,逐行扫描) RR 下的做法 每扫一行加一把 Next-Key Lock,不释放,全表锁死到事务提交 —— 防幻读必须宁可错杀 后果:无关行全部被锁,写并发归零 RC 下的做法(semi-consistent read) 逐行短暂加锁做 WHERE 判断 → 不满足条件的行立即释放锁 → 最终只锁住真正命中的行 后果:锁窗口极短,并发几乎不受影响(代价:不防幻读)
延伸一句:这正是很多互联网公司把隔离级别从 RR 改成 RC 的又一理由——除了缩小间隙锁范围,半一致读还让「无索引 / 索引选择性差」的 UPDATE 不再成为全表锁事故。当然,RC 需要配合 ROW 格式的 binlog,且放弃了幻读保护。
6
如何避免这种事故的发生?
单点手段都靠不住,要织一张「事前 → 事中 → 事后」的网。
时机手段怎么做
事前
(设计期)
治本 给 WHERE 字段建索引 排查所有高频 UPDATE / DELETE 的 WHERE 列,逐个确认有索引。这是唯一能根治的手段。
推荐 隔离级别评估 高并发写入、可接受幻读的业务,评估改用 RC:间隙锁消失 + 半一致读兜底,同类事故自然免疫大半。
兜底 大事务拆小 批量 UPDATE 分批提交(如每 1000 行一批),即使误锁全表,持锁时间也被限制在毫秒级。
事前
(上线前)
必做 EXPLAIN 检查 每条上线的 UPDATE / DELETE 先 EXPLAIN,type=ALL 且表很大 → 直接打回。也可在 SQL 审核平台里强制卡点。
必做 SQL 审核卡点 把「UPDATE/DELETE 必须走索引」写进上线流程(人工评审或自动化审核),不靠个人自觉。
一行配置 安全更新模式 SET GLOBAL sql_safe_updates=1; 之后不带索引条件的 UPDATE/DELETE 直接报错(除非带 LIMIT)。开发环境强烈建议开启。
事中
(运行期)
慢日志兜底 开启 log_queries_not_using_indexes,未走索引的 SQL 全部记入慢日志,主动发现而不是等事故。
监控锁等待 盯 sys.innodb_lock_waits 与连接池水位;「写请求突然全部变慢」往往就是全表锁的第一症状。
事后 应急 + 复盘 应急:KILL 阻塞源(先查 innodb_trx 找到未提交的长事务)。复盘:为什么没索引的 SQL 上得了线?补流程。
-- 安全更新模式:没有索引条件的 UPDATE/DELETE 直接报错
SET GLOBAL sql_safe_updates = 1;
UPDATE t_user SET name='X' WHERE name='张三';
-- ERROR 1175 (HY000): You are using safe update mode ...
-- (name 无索引且未带 LIMIT → 直接拒绝)

-- 慢日志:把没走索引的 SQL 全抓出来
SET GLOBAL log_queries_not_using_indexes = ON;
SET GLOBAL long_query_time = 0.5;
7
交互:危险 UPDATE 检测器
模拟 sql_safe_updates 的判定逻辑 + 事故风险等级,选条件看结论。
WHERE 条件 带索引列 带非索引列 没有 WHERE
LIMIT 限制 无 有 LIMIT
隔离级别 可重复读 RR 读已提交 RC
安全:只锁命中行
可控:锁范围可评估
危险:等效锁全表
8
总结

一句话收尾

  • 会锁全表吗?RR 下会——不是表锁,而是全表每条记录都被加了 Next-Key Lock,阻塞效果等同表锁;普通 SELECT 因快照读不受影响。
  • 为什么发生?行锁加在索引上 → 无索引只能全表扫描 → RR 下扫过即锁(防幻读宁可错杀)→ 写操作全部排队 → 连接池耗尽 → 雪崩。
  • RC 为什么没事?半一致读让「不满足条件的行」锁后立即释放,最终只锁命中行。
  • 怎么避免?治本一条:WHERE 字段建索引;流程两条:EXPLAIN 上线卡点 + sql_safe_updates;兜底三条:慢日志、批量拆分、评估改 RC。

速记口诀

  • 行锁锁在索引上,没索引就锁全表
  • RR 扫过即锁到提交,RC 试完即放只锁命中
  • 快照读不怕,怕的是写
  • 上线先 EXPLAIN,type=ALL 必打回

三个易错点

  • 「锁全表」≠ 表锁——是很多把行锁,别和 MyISAM 表锁混淆
  • 别以为只影响「这张表的写」——连接池被占满会殃及全应用
  • 别以为小表没事——业务量涨了、扫过的行多了,同样的 SQL 迟早炸

关联知识

  • 为什么扫过即锁:见本系列第二篇「万能加锁规则」
  • Next-Key / Gap 是什么:见第一篇速查手册
  • RC 与 RR 的取舍:隔离级别专题