直接delete不加limit是i/o炸弹;event串行执行易阻塞且不感知i/o压力;应由外部脚本分批执行delete limit 1000并检查row_count()。

DELETE语句不加LIMIT就是I/O炸弹
直接 DELETE FROM logs WHERE created_at 会锁表、占满undo log、触发大量随机磁盘写,尤其在机械盘或高并发库上,可能拖垮整个实例。MySQL不是为单次扫千万行删数据设计的。
真正可行的做法是切片 + 控制节奏:
- 每次只删
1000行(LIMIT 1000必须有,且需配合主键/索引字段做游标) - 删完立刻
COMMIT,释放锁和undo空间 - 用
SLEEP(0.1)或更长间隔(单位秒),给I/O喘息时间;别用SELECT SLEEP(),它会阻塞连接,改用应用层休眠 - 避免用
OFFSET分页(LIMIT 1000 OFFSET 10000越往后越慢),改用WHERE id > ? ORDER BY id LIMIT 1000
为什么不能依赖事件调度器自动跑大DELETE
MySQL EVENT 默认在单一线程里串行执行,一旦某次删除卡住(比如遇到长事务阻塞、索引失效),后续所有事件堆积,反而加剧延迟和锁等待。它不感知I/O压力,也不会自动降频。
更稳妥的方式是把分段逻辑交给外部脚本控制:
- Python/Shell脚本调用
mysql -e "DELETE ... LIMIT 1000",每次执行后检查ROW_COUNT()是否为 0(表示删完了) - 用
SELECT MAX(id) FROM ... WHERE created_at 预估总量,但别拿来分页,只作进度参考 - 监控
Innodb_row_lock_waits和Threads_running,超过阈值就暂停下一轮
ORDER BY + LIMIT 删除时索引失效的典型陷阱
写成 DELETE FROM logs WHERE created_at 看似合理,但 MySQL 5.7+ 在这种 DELETE 中往往无法利用 <code>created_at 索引做范围扫描,转而全表扫描再过滤,CPU和I/O双爆。
正确姿势是显式用主键推进:
DELETE FROM logs WHERE id <p>但更推荐拆成两步:先查出一批 <code>id</code>(<code>SELECT id FROM logs WHERE created_at ),再 <code>DELETE FROM logs WHERE id IN (...)</code> —— 这样能确保走主键索引,且方便加 <code>FOR UPDATE</code> 避免重复删。</code></p> <h3>sleep间隔不是越长越好,要看binlog和复制延迟</h3> <p>设成 <code>SLEEP(1)</code> 看似安全,但如果主库启用了 <code>binlog_format=STATEMENT</code>,每个 <code>DELETE ... LIMIT</code> 会被记为独立事件,从库重放时可能因顺序或并发问题导致数据不一致;而 <code>ROW</code> 格式虽安全,但大事务拆小后,binlog文件增长更快,备份和传输压力上升。</p> <p>实操建议:</p>
- 优先用
binlog_format=ROW,并确认从库slave_parallel_workers > 0 - sleep 时间建议从
0.05起步,观察Seconds_Behind_Master和io_utilization(如iostat -x 1中 %util)再微调 - 别在高峰期运行;如果必须在线清理,提前在从库验证相同SQL的I/O表现
最易被忽略的一点:删除过程中如果表上有 ON DELETE CASCADE 外键,每删一行都可能触发额外查询和更新,实际I/O放大远超预期——这种场景务必先禁用外键或改用归档迁移方式处理。











