InnoDB DELETE未走索引时等价于锁全表,因其被迫全表扫描并对每行加行锁+间隙锁;根本原因是WHERE字段无索引、索引失效或优化器误判。

DELETE没走索引时,InnoDB为什么等价于锁全表
不是MySQL“故意”锁表,而是InnoDB在无法用索引定位行时,被迫做全表扫描,同时为每行加行锁 + 间隙锁(gap lock),最终效果和锁表无异。关键在于:WHERE条件字段没索引、索引失效(如隐式类型转换、函数包裹)、或优化器误判导致未走索引。
常见错误现象:
-
SHOW PROCESSLIST里看到Deleting状态长时间卡住,其他查询被阻塞 -
SELECT * FROM information_schema.innodb_trx显示事务持有大量LOCK_MODE为X,REC_NOT_GAP或X,GAP的锁 -
EXPLAIN结果中type是ALL,key为NULL
如何确认DELETE是否触发了全表扫描加锁
执行前必须验证执行计划,不能只看语句长得像“按ID删”。重点查三件事:
- 对目标
DELETE语句改写为SELECT,跑一次EXPLAIN:EXPLAIN DELETE FROM t WHERE status = 'old';→ 改成EXPLAIN SELECT * FROM t WHERE status = 'old'; - 检查
status字段是否有有效索引:SHOW INDEX FROM t WHERE Key_name != 'PRIMARY';,注意索引是否被WHERE左侧函数破坏(如WHERE UPPER(status) = 'OLD') - 确认隔离级别不是
SERIALIZABLE——该级别下普通SELECT也会加范围锁,影响更大
分批删除时仍锁表?可能是事务没及时提交
分批本身不解决锁问题,只是把大锁拆成多个小锁;但如果每个批次都在一个长事务里,锁会一直持有着,直到COMMIT。更糟的是,自动提交关了(autocommit=0)却忘了COMMIT,等于把所有批次的锁都堆在一起。
实操建议:
- 显式控制事务:
BEGIN; DELETE FROM t WHERE id BETWEEN 1000 AND 2000; COMMIT;,别依赖默认行为 - 用
LIMIT但必须配合ORDER BY确保可重复性,否则可能漏删或重删:DELETE FROM t WHERE status = 'old' ORDER BY id LIMIT 1000; - 监控事务时长:
SELECT trx_id, trx_started, trx_state, trx_rows_locked FROM information_schema.innodb_trx WHERE trx_started
为什么加了索引还锁表?留意隐藏的锁升级条件
有索引≠安全。以下情况仍会导致锁范围扩大甚至退化:
- 唯一索引查不到记录时,InnoDB会加间隙锁锁定“本该存在”的位置(比如
id = 5不存在,则锁(3,7)区间),高并发下容易引发锁等待 - 联合索引未用最左前缀,例如索引是
(a,b,c),但WHERE b = 1跳过了a,索引失效 - 字符集/排序规则不一致导致隐式转换:
status字段是utf8mb4_unicode_ci,而参数传的是latin1,索引无法使用 - 统计信息过期:
ANALYZE TABLE t;能强制更新,避免优化器误判走错索引
真正危险的从来不是“删得慢”,而是“删着删着,别的业务全卡住了”——锁的可见性差、排查链路长,往往要等到报警才意识到问题。盯住EXPLAIN、盯住information_schema.innodb_trx、盯住事务生命周期,比事后杀进程有用得多。











