用 where id > last_id order by id limit n 分批删除最稳妥,因其避免间隙锁扩散、确保无重叠推进、强制走主键索引、锁粒度可控;需配合应用层休眠(如 sleep(0.1)~sleep(0.5))缓解锁冲突,并确保删除条件字段有合适索引(如 index(status, id)),且 last_id 必须来自上批实际删除的最大 id。

直接结论:用 WHERE id > last_id ORDER BY id LIMIT N 逐步推进,比 WHERE id BETWEEN x AND y 或 OFFSET 更稳,能避开间隙锁扩散和索引扫描偏移导致的死锁。
为什么 WHERE id BETWEEN 容易引发死锁
当使用 id BETWEEN 10000 AND 20000 删除时,InnoDB 可能对范围内的所有行加记录锁 + 间隙锁,尤其在 id 不连续或存在并发插入时,间隙锁会覆盖未命中行之间的空档,多个事务若扫描重叠区间(比如 A 删 10000–20000,B 同时删 19999–29999),极易形成循环等待。更糟的是,如果该范围跨多个索引页,还可能触发锁升级。
- 不依赖主键连续性,
id > last_id天然保证每次只向后推进,无回溯、无重叠 -
ORDER BY id LIMIT N强制走主键索引,避免优化器选错索引导致全表扫描加锁 - 每批只锁定实际命中的 N 行及右侧一个间隙,锁粒度可控
DELETE ... LIMIT 必须配 SLEEP 才有效
光加 LIMIT 不够——事务提交快,但下一批立刻执行,MySQL 线程调度密集,CPU 和日志写入压力集中,容易让其他事务排队等待锁释放,反而放大冲突概率。真实生产中,应用层控制休眠比存储过程内 SLEEP() 更可靠(后者受 max_execution_time 限制,且难以监控)。
- 建议每批删完后
sleep(0.1)~sleep(0.5),机械硬盘环境可拉长到sleep(1) - 别用
SELECT SLEEP(0.1)在 SQL 层硬塞,它计入事务时间,可能拖长锁持有 - 监控
innodb_row_lock_waits和innodb_deadlocks,休眠值应随负载动态调整
删除条件字段没索引?分批也白搭
如果 WHERE 条件字段(比如 status = 'archived')没索引,InnoDB 每次都得全表扫描再过滤,即使加了 LIMIT 1000,照样要遍历并锁住大量无关行——这些“误锁”正是死锁温床。
- 先确认执行计划:
EXPLAIN DELETE FROM t WHERE status = 'archived' LIMIT 1000,看type是否为range或ref - 若无索引,优先建
INDEX (status, id)(联合索引,id用于排序+分页) - 切忌建单列
INDEX(status)后还用ORDER BY created_at——会触发 filesort,锁范围失控
真正难处理的不是“怎么删”,而是“删到哪了”。last_id 必须来自上一批删掉的最大 id 值(不是 MAX(id) 全表查),否则漏删或重复删;这个值一旦在应用层丢失,后续就只能靠 SELECT id FROM t WHERE ... ORDER BY id LIMIT 1 重新定位——而这条语句本身又可能被锁住。











