根本原因是rr隔离级别下next-key lock导致全表扫描即全表锁;需确保索引生效、autocommit开启或手动commit、order by单调字段锚定,并建议改用read committed减少锁范围。

直接用 DELETE FROM t WHERE ... 删几十万行,大概率触发锁等待超时或阻塞业务,根本原因不是“删得慢”,而是 RR 隔离级别下 MySQL 对扫描范围加了 next-key lock —— 全表扫就等于全表锁。
为什么分批删还卡住:autocommit 和事务没关好
很多人加了 LIMIT 还是卡,是因为显式开了事务但忘了 COMMIT。MySQL 默认 autocommit=1,每个带 LIMIT 的 DELETE 是独立小事务;一旦你执行过 START TRANSACTION 或代码里开启了事务连接池会话,后续所有 DELETE 就都在一个长事务里,锁不释放,binlog/undo log 持续膨胀。
- 查当前会话状态:
SELECT @@autocommit;,值为 0 就要格外小心 - 分批循环中,每次
DELETE后必须跟COMMIT(哪怕只删 100 行) - 应用层用连接池时,确认是否复用了已开启事务的连接(比如 Spring 的
@Transactional未正确退出)
ORDER BY + 单调字段锚定:否则会漏删或重复删
只写 DELETE FROM t WHERE status = 0 LIMIT 1000 是危险的 —— InnoDB 不保证无 ORDER BY 时的扫描顺序,尤其在并发写入下,同一批可能反复删 id=100~200 的行,跳过 id=5000+ 的行。
- 必须配合单调递增字段(如
id)和ORDER BY:DELETE FROM t WHERE status = 0 AND id > 12345 ORDER BY id LIMIT 1000 - WHERE 条件必须能走索引,推荐建联合索引:
INDEX idx_status_id (status, id) - 每次执行后记录最后删掉的
id值,作为下一轮的锚点
索引失效导致分批失效:每轮都在全表扫
如果 WHERE 中的字段没索引,或者条件用了函数(如 DATE(create_time) = '2025-01-01'),MySQL 每次仍要全表扫描找 1000 行,性能比单次大删还差。
- 用
EXPLAIN看执行计划:type是ALL或rows接近表总行数,说明没走索引 - 避免在索引列上做计算、类型转换、
LIKE '%xxx' - 若过滤字段基数低(如
status只有 0/1),务必搭配主键或时间字段组成覆盖索引
READ COMMITTED 能显著减少锁范围
线上环境还在用默认的 REPEATABLE READ?那 DELETE 会加间隙锁(gap lock),哪怕只删 1 行,也可能锁住相邻空闲区间,引发更多等待。
- 改成
READ COMMITTED后,InnoDB 只对实际命中的行加 record lock,不加 gap lock - 执行前设会话级隔离级别:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; - 注意:RC 下不可重复读是允许的,多数日志清理、归档类场景完全可接受
真正容易被忽略的,是索引是否真正生效 + autocommit 是否被框架静默关闭 + 锚点字段是否真单调 —— 这三处任一出错,分批就退化成“假装优化”。











