根本原因是InnoDB对所有扫描行(含间隙)加next-key锁,无索引时全表扫描导致锁住海量行与间隙,等效锁表;分批需配合索引+ORDER BY+单调字段锚点确保可控锁粒度与避免重复扫描。

直接在大表上执行 DELETE 会锁住大量行甚至等效锁表,根本原因不是“删得多”,而是“扫得广、锁得久、不释放”。优化锁占用流程的关键,是让每次操作只触达必要数据,并立刻交还锁资源。
为什么没索引的 DELETE 等于锁表
InnoDB 在 DELETE 时会对**所有扫描到的行(含间隙)加 next-key lock**,而不仅限于最终删除的行。没有索引时,MySQL 只能全表扫描——几千万行逐行判断、加锁、写 undo log、维护 MVCC 版本链。这时:
- 事务长时间不提交,其他查询/更新被阻塞
- undo log 持续膨胀,purge 线程跟不上,进一步拖慢整个实例
- 主从复制延迟可能瞬间飙升至小时级
哪怕 WHERE 条件字段有索引,若匹配行数过多(比如删掉 90% 的数据),优化器也可能放弃索引走全表扫描——务必先 EXPLAIN DELETE 确认执行计划。
分批删除必须配合 ORDER BY + 单调字段锚点
只加 LIMIT 不够,DELETE ... LIMIT 1000 在无排序时无法保证顺序,可能反复删同一组行、跳过另一些,尤其在并发写入场景下漏删严重。
正确做法是用单调递增字段(如 id)做边界锚定:
DELETE FROM orders WHERE status = 'expired' AND id > 12345678 ORDER BY id LIMIT 10000;
每轮执行后记录本次删到的最大 id,下一轮作为新起点。这样:
- 每次只扫描索引中一段连续范围,锁粒度可控
- 避免重复扫描已处理数据,减少 I/O 和 CPU 开销
- 配合
COMMIT后,这批锁和 undo log 立即释放
autocommit=0 是隐形锁陷阱
很多人在存储过程中设 SET autocommit = 0,再手动 COMMIT,但一旦逻辑出错或异常退出,事务不会自动回滚,锁会一直挂着直到会话断开——这比慢查询更危险。
更稳妥的做法是:
- 显式开启事务:
START TRANSACTION - 删完立刻
COMMIT,不要依赖会话生命周期 - 避免在事务内做非数据库操作(如调用外部 API、循环 sleep)
- 在低峰期运行,监控
INFORMATION_SCHEMA.INNODB_TRX确认无长事务残留
真正省锁的替代方案:分区表 DROP PARTITION
如果删除逻辑天然按时间/范围切分(如日志表删 3 个月前数据),建好 RANGE 或 LIST 分区后,直接执行:
ALTER TABLE logs DROP PARTITION p202606;
这个操作:
- 不走索引、不写 undo log、不加行锁
- 秒级完成,对业务几乎无感知
- 要求分区字段必须出现在 WHERE 条件中,且不能与主键冲突(例如主键为
(id, create_time),分区字段可用create_time)
注意:分区不是银弹。分区键设计不当会导致查询无法裁剪,反而更慢;迁移历史表需停写或使用 pt-online-schema-change 工具。
最易被忽略的一点:即使用了索引+分批,如果 innodb_lock_wait_timeout 设置过短,或从库存在延迟,仍可能因主从不一致触发唯一键冲突或死锁。上线前务必在从库验证语句是否可重放,而不是只在主库测通。


















