EXPLAIN DELETE语句是验证索引是否生效的唯一可靠方式,因MySQL对DELETE与SELECT执行计划生成逻辑不同,同一WHERE条件可能SELECT走索引而DELETE全表扫描。

EXPLAIN DELETE 语句比猜更可靠
别信“这个字段明明建了索引”,DELETE是否真走索引,必须用EXPLAIN验证。MySQL 对 DELETE 和 SELECT 的执行计划生成逻辑不同,同一 WHERE 条件下,SELECT 走索引,DELETE 却可能全表扫描。
常见错误现象:删一行卡住、并发写入大量超时、information_schema.INNODB_TRX 中 TRX_ROWS_LOCKED 接近表总行数。
- 直接运行
EXPLAIN DELETE FROM t WHERE status = 'old',重点看:type是否为ALL、key是否为NULL、rows是否接近表总行数 - 若子查询出现在
WHERE ... IN (SELECT ...)中,大概率失效;改用DELETE t1 FROM t1 JOIN t2 ON ... WHERE ...更稳 - 函数包裹或隐式转换会让索引彻底失效:如
WHERE DATE(create_time) = '2024-01-01'或WHERE user_id = 123(但user_id是VARCHAR)
索引不是建了就有效,得匹配查询模式
复合索引 (a, b, c) 对 WHERE b = 1 AND c = 2 无效,因为跳过了最左前缀 a;单列索引 idx_status 对 WHERE status = ? AND create_time > ? 也撑不住,需要联合索引 idx_status_created。
真正起作用的索引,必须覆盖 WHERE 中所有等值条件,并尽量延伸至排序/分页字段(如 ORDER BY id)。
- 高频删除场景推荐建联合索引,例如清理旧数据:
INDEX idx_status_id (status, id) -
status基数低(如只有 0/1)时,单独索引效率差,必须搭配单调字段(如自增id或时间戳)提升区分度 - 建完索引立刻跑
ANALYZE TABLE t更新统计信息,否则优化器可能因过期数据继续选错路径
分批删 ≠ 自动解锁,关键在事务边界和锚点
加 LIMIT 1000 不解决锁膨胀问题,除非每一批都在独立事务里提交。autocommit 关闭状态下,多轮 DELETE 累积持有锁,直到最终 COMMIT 才释放。
更隐蔽的问题是:没 ORDER BY 的分批删,在并发写入下可能重复删或漏删——InnoDB 不保证无序扫描的稳定性。
- 必须显式控制事务:
BEGIN; DELETE FROM t WHERE status = 0 AND id > 10000 ORDER BY id LIMIT 1000; COMMIT; - 每次删完记录最后处理的
id值(如SELECT LAST_INSERT_ID()或应用层捕获),作为下一轮WHERE id > ?的锚点 - 确认连接池或 ORM 框架没静默开启长事务(如 Spring 的
@Transactional未正确退出)
READ COMMITTED 隔离级别能大幅减少锁范围
默认的 REPEATABLE READ 在 DELETE 时会加 next-key lock(记录锁 + 间隙锁),哪怕只删一行,也可能锁住前后空闲区间;换成 READ COMMITTED 后,InnoDB 只对实际命中的行加 record lock,不加 gap lock。
这对日志归档、状态清理类任务几乎零副作用,但要注意:RC 下不可重复读是允许的,不适合强一致性业务场景。
- 会话级生效:执行
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;再删 - 服务端配置(可选):
transaction_isolation = READ-COMMITTED写入my.cnf,避免每个连接手动设 - 切勿仅依赖
FORCE INDEX:它绕过优化器判断,却掩盖了索引设计或数据分布的真实问题
EXPLAIN结果里那个key=NULL,以及应用代码里那个没写的COMMIT。锁不是突然出现的,是一行行扫出来、一次次没释放堆出来的。


















