MySQL范围查询更新不会锁整表,但未命中索引会导致全表扫描并加Next-Key锁,等效锁表;根本原因是InnoDB行锁依赖索引,无索引时须逐行加X锁与间隙锁。

MySQL范围查询更新不会“锁住整张数据表”,但可能效果等同于锁表——根本原因是 WHERE 条件未命中有效索引,或索引设计/使用不当,导致 InnoDB 退化为全表扫描,并对每一条扫描到的聚簇索引记录加 X 锁 + Gap 锁(即 Next-Key Lock)。
真正锁表的不是“范围查询”本身,而是没走索引的范围扫描。只要索引能精准截断扫描边界,锁就只落在目标区间内。
UPDATE 范围条件没走索引 → 全表扫描 + 全量行锁
InnoDB 行锁必须依托索引加在 B+ 树节点上。当 WHERE 字段无索引,或索引失效时:
- 优化器选择 type = ALL 执行计划,Server 层逐行送记录给引擎层判断
- 引擎层对每一条被扫描的聚簇索引记录都加 X 锁(记录锁)和 Gap 锁(间隙锁)
- 在 RR 隔离级别下,这等效于锁住整个主键索引范围:从最小 id 到最大 id,连带所有间隙
-
SHOW ENGINE INNODB STATUS中trx_rows_locked值会接近表总行数,是典型信号
常见索引失效场景:
-
WHERE YEAR(created_at) = 2024→ 改成created_at >= '2024-01-01' AND created_at -
WHERE user_id = '123'(user_id是BIGINT)→ 字符串触发隐式转换,索引丢弃 - 联合索引
idx_status_ctime(status, created_at),却查WHERE created_at > '2026-01-01'→ 不满足最左前缀,索引不可用
走了索引但范围过大 → 锁住大片索引区间
即使 EXPLAIN 显示用了索引,若扫描行数(rows)远高于实际匹配数(filtered),仍会锁住大量无关记录:
- 比如联合索引
idx_ab(a,b),执行UPDATE t SET c=1 WHERE a = 1 AND b > 5 - InnoDB 定位到
a = 1子树后,线性遍历所有b值直到超出范围,期间对每个扫描到的节点加锁 - 实际更新的可能只有
(1,7)、(1,9),但(1,3)、(1,5)的间隙也被锁(防止插入(1,4))
验证方法:
- 另起事务执行
INSERT INTO t VALUES (1, 4, 100)→ 若阻塞,说明(1,3)→(1,5)间隙已被锁 -
SELECT * FROM t WHERE a = 1 AND b BETWEEN 4 AND 6 FOR UPDATE→ 若阻塞,说明该区间已被 Next-Key 锁覆盖
如何避免范围更新锁扩散?
核心思路:让索引能精确圈定扫描起点和终点,且控制单次事务处理量
- 建联合索引时把过滤性最强的字段放最左,例如按
status和created_at更新,建INDEX idx_status_ctime_id (status, created_at, id) - 查询必须严格最左匹配:
WHERE status = 'pending' AND created_at >= '2026-06-01' AND created_at - 分批更新:用主键游标切分,每次
UPDATE ... WHERE id BETWEEN ? AND ?,步长控制在 1000–5000,每批立刻COMMIT - 确认隔离级别:业务允许幻读时,可设为
READ COMMITTED,它不加 Gap 锁,只锁命中的记录
关键点容易被忽略:锁范围不是由 SQL 写法决定的,而是由实际走哪条索引路径、扫描了多少索引节点决定的。哪怕只更新一行,只要扫描了十万行,就锁了十万行——优化的第一步永远是看 EXPLAIN FORMAT=tree,而不是猜。


















