delete百万行卡死是因rr级别下全表扫描触发next-key lock全表锁定,叠加binlog/undo log膨胀阻塞purge线程;分批删须用order by+单调字段锚定,且where条件必须命中联合索引(status,id),配合sleep避免主从延迟。

DELETE 语句直接删百万行为什么卡死
因为 MySQL 默认在可重复读(RR)隔离级别下,DELETE 会为扫描到的每行加 next-key lock,全表扫 = 全表锁。更糟的是,大事务会拖长 binlog 和 undo log 生命周期,阻塞 purge 线程,进一步拖慢后续操作。
- 现象:执行
DELETE FROM t WHERE status = 0卡住,SHOW PROCESSLIST显示Updating状态持续数分钟 - 本质不是“慢”,是“锁冲突+日志膨胀”双重阻塞
- 哪怕加了
status索引,如果匹配行太多,MySQL 仍可能放弃索引走全表扫描(看EXPLAIN的rows和type)
用 LIMIT 分批删但没加 ORDER BY 会漏删
分批删不是加个 LIMIT 就完事。InnoDB 不保证无 ORDER BY 时的物理顺序,同一批可能反复删同一组行,跳过另一些——尤其在并发写入场景下。
- 错误写法:
DELETE FROM t WHERE status = 0 LIMIT 1000(循环执行)→ 可能永远删不完 - 正确做法:锚定一个单调递增字段(如
id),每次删id > last_id AND status = 0的最小 1000 行 - 必须加
ORDER BY id LIMIT 1000,否则优化器可能改变扫描顺序 - 示例:
DELETE FROM t WHERE status = 0 AND id > 12345 ORDER BY id LIMIT 1000
WHERE 条件没走索引导致分批失效
分批的前提是每次都能快速定位起始位置。如果 WHERE 中的过滤字段没索引,或组合条件无法命中索引最左前缀,MySQL 每次仍要全表扫描找 1000 行,性能不升反降。
- 检查方式:
EXPLAIN DELETE FROM t WHERE status = 0 AND id > 1000 ORDER BY id LIMIT 1,确认key列非NULL - 常见坑:
status是低基数字段(如只有 0/1),单独建索引效果差;应建联合索引(status, id) - 注意:如果
id是主键,(status, id)索引比(id, status)更适合这个查询模式
sleep 间隔太短反而加重主从延迟
分批删本身会生成大量 binlog event,如果每秒删多批、不 sleep,从库重放压力陡增,可能触发 Seconds_Behind_Master 暴涨,甚至复制中断。
- 建议单批执行后
SLEEP(0.1)(100ms),让从库喘口气 - 不要用
SELECT SLEEP(1)这种阻塞式调用,改在应用层控制节奏 - 监控指标:删的过程中观察
SHOW GLOBAL STATUS LIKE 'Com_delete'增速,以及Innodb_row_lock_time_avg是否突升 - 极端情况:考虑临时关闭 binlog(
SET sql_log_bin = 0),但仅限从库或允许数据不一致的归档场景











