innodb在delete未走索引时,对全表扫描到的每一行聚簇索引记录加next-key锁(记录锁+间隙锁),锁行数接近全表且覆盖所有插入间隙,等效锁表;根本原因是优化器退化为全表扫描,而非真正加表锁。

DELETE没走索引时,InnoDB到底在锁什么?
不是MySQL“锁了整张表”,而是InnoDB在全表扫描过程中,对扫描到的每一行都加了next-key lock(记录锁 + 间隙锁),最终效果等同于锁表——因为锁住的行数接近全表,且锁范围覆盖所有可能插入的位置。
关键点在于:没有索引支撑的WHERE条件,会让优化器放弃索引查找路径,退化为聚簇索引(主键B+树)的顺序遍历。每扫一行,就加一把锁;扫完几十万行,就持有几十万个锁。其他事务只要想修改任意一行(哪怕和你要删的完全无关),都可能被阻塞。
-
EXPLAIN SELECT * FROM t WHERE status = 'old'中type=ALL、key=NULL、rows接近表总行数 → 基本可断定会触发锁膨胀 - 即使只删1行,只要没索引,也会扫全表加锁——InnoDB不预判结果集大小,只按执行计划走
- RR隔离级别下更危险:间隙锁会封锁
status='old'值可能出现的所有区间,进一步扩大阻塞面
哪些写法会让索引“形同虚设”?
建了索引≠能用上。很多看似合理的WHERE条件,实际让索引完全失效,导致隐式全表扫描。
-
WHERE DATE(create_time) = '2024-01-01':函数包裹字段,索引失效 -
WHERE user_id = '123'(user_id是INT):隐式类型转换,索引失效 -
WHERE status != 'active':非等值判断,通常无法有效利用B+树索引 - 复合索引
(a, b, c),但查询是WHERE b = 1 AND c = 2:跳过最左前缀,索引失效
验证方式很简单:把DELETE语句改写成SELECT *,跑一次EXPLAIN。别信“应该能走索引”,要看真实执行计划。
分批删除为什么还是卡住?
分页删(比如LIMIT 1000)本身不解决锁问题,只是把一个大锁拆成多个小锁——但如果这些小锁都在同一个未提交事务里,它们会累积持有,直到COMMIT才释放。
- 常见错误:
autocommit=0下执行多轮DELETE ... LIMIT 1000,却忘了COMMIT - 更隐蔽的问题:分批逻辑写在应用层,但每次删除后没及时提交,事务持续打开 → 锁一直挂着
- 即使每批都
COMMIT,若WHERE条件仍无索引,每批仍是全表扫描 → 每次都锁大量行,高并发下依然互相阻塞
真正有效的分批,必须满足两个前提:有可用索引 + 每批独立事务提交。
怎么确认当前是不是已经锁表了?
别靠猜,直接查锁状态。重点看三个指标:
-
SELECT * FROM information_schema.INNODB_TRX:关注TRX_ROWS_LOCKED(锁行数)是否远大于TRX_ROWS_MODIFIED(实际修改行数) -
SHOW PROCESSLIST:看是否有多个线程卡在Deleting或Updating状态,且Time持续增长 - 在另一个会话执行
ALTER TABLE t ADD COLUMN x INT:如果立刻被卡住,基本说明当前有事务持有表级意向锁(IX),且未释放
注意:INNODB_LOCKS 在 MySQL 5.7+ 已废弃,8.0+ 要查 performance_schema.data_locks。但日常排查,INNODB_TRX + PROCESSLIST 就够用。
锁表不是配置或版本问题,是执行路径和索引设计共同决定的结果。最容易被忽略的是:你改了SQL、加了索引、甚至调了sql_safe_updates,但没重跑EXPLAIN验证——那一切优化都只是自我安慰。











