索引失效时update/delete会扩大锁范围,innodb退化为全表或全索引扫描并对所有扫描到的聚簇索引记录加next-key lock或gap lock,导致锁行数激增、并发阻塞甚至死锁。

MySQL 索引失效时,UPDATE/DELETE 确实会扩大锁范围
会。当 WHERE 条件无法命中索引(即索引失效),InnoDB 无法精确定位记录,就会退化为全表扫描或全索引扫描,进而对扫描过程中遇到的**所有聚簇索引记录(即实际数据行)加临键锁(Next-Key Lock)或间隙锁(Gap Lock)**——哪怕最终只修改/删除其中一行。
这意味着:原本只锁 1 行 → 可能锁住几百、几千行,甚至整个索引区间;并发更新容易阻塞,还可能引发死锁。
- 常见触发场景:
WHERE name LIKE '%abc'(左模糊)、WHERE status + 0 = 1(隐式类型转换)、WHERE created_at > '2024-01-01' AND deleted = 0但(created_at, deleted)复合索引顺序是(deleted, created_at) - 验证方法:执行
SHOW ENGINE INNODB STATUS\G,看TRANSACTIONS部分的lock_mode和lock_trx_id,结合LOCK WAIT或RECORD LOCKS行数判断锁范围 - 注意:即使
SELECT ... FOR UPDATE没走索引,同样会扩大锁范围,不只是 DML
如何快速确认 WHERE 条件是否走索引
别猜,用 EXPLAIN 看执行计划,重点关注 type、key、rows 和 Extra 字段。
-
type是ALL或index(全表/全索引扫描)→ 基本没走有效索引 -
key显示NULL→ 没用上任何索引 -
rows数值远大于实际匹配行数 → 索引选择性差或未生效 -
Extra出现Using filesort或Using temporary不直接代表索引失效,但常伴随低效扫描;出现Using index condition是好信号(ICP 生效)
缩小锁范围的关键操作:让 WHERE 精准命中索引
核心不是“加更多索引”,而是让查询条件能利用**最左前缀 + 等值 + 范围**的天然索引结构。
- 避免在索引列上做运算:
WHERE YEAR(create_time) = 2024→ 改成WHERE create_time >= '2024-01-01' AND create_time - 避免隐式转换:
WHERE user_id = '123'(user_id 是 INT)→ 改成WHERE user_id = 123 - 复合索引顺序必须匹配查询模式:
WHERE a = 1 AND b > 10,索引应建为(a, b),而非(b, a);若还有c排序需求,ORDER BY c无法复用该索引,需考虑覆盖索引 - LIKE 左模糊必然失效:
name LIKE '%abc'无法走索引;若业务允许,改用全文索引或倒排表,或前置加固定前缀(如name LIKE 'abc%')
锁范围还受事务隔离级别和语句类型影响
即使索引有效,锁行为也不同:读已提交(RC)下普通 SELECT 不加锁,但 UPDATE/DELETE 仍只锁命中的索引记录;可重复读(RR)下默认加 Next-Key Lock,锁住记录+间隙。
- RR 下,
WHERE id = ?(主键等值)→ 只锁单行(记录锁) - RR 下,
WHERE name = ?(非唯一索引等值)→ 锁该值对应的所有行 + 相邻间隙(防止幻读) - RC 下,同上语句只锁命中的行,不锁间隙;但 MySQL 8.0+ 在 RC 下对唯一索引等值查询也会加间隙锁以避免主从不一致,需实测版本
- 使用
SELECT ... FOR UPDATE时,即使索引有效,也要注意是否带ORDER BY或LIMIT——LIMIT 1不会减少锁范围,InnoDB 仍会扫描到满足条件的第一行为止,期间锁住所有扫描过的记录
EXPLAIN 要像看日志一样习惯,而不是等到超时才去翻。











