delete千万行数据必须分批执行,且where条件字段须有索引;无索引时全表扫描会导致锁行、超时及性能恶化;需结合explain评估执行计划,并预先确认主键游标策略、外键约束、触发器及binlog空间等风险点。

DELETE 不能直接删千万行,否则锁表、日志爆满、主从延迟飙升是大概率事件。必须分批,且每批要可控、可中断、不抢资源。
WHERE 条件字段没索引就别动手
没索引的 created_at 或 status 字段在 DELETE 中用作筛选条件,每次 LIMIT 前都要全表扫描——不仅慢,还会让 MySQL 锁住大量无关行,甚至触发 Lock wait timeout exceeded。
- 先用
EXPLAIN SELECT * FROM your_table WHERE created_at 确认是否走索引 - 没索引就加:
ALTER TABLE your_table ADD INDEX idx_created_at (created_at) - 加索引本身会锁表,线上务必选低峰期,MySQL 5.6+ 可用
ALGORITHM=INPLACE降低影响
存储过程里用 ROW_COUNT() 判断退出,别硬写循环次数
靠 WHILE i 这种固定次数循环极易漏删或空跑——因为最后一批可能不足 1000 行,<code>ROW_COUNT() 才是真实反馈。
- 每次
DELETE ... LIMIT 1000后立即SELECT ROW_COUNT() INTO deleted_rows -
deleted_rows = 0就EXIT,不依赖预估总量 - MySQL 5.7+ 支持
LIMIT后接变量,但低版本不支持,所以别写LIMIT batch_size,老实用常量
每批提交节奏得看耗时,不是看行数
“每删 1000 行 COMMIT” 是常见误区:太频繁放大事务开销;太稀疏(比如 5 万行一提交)又可能撑爆 innodb_undo_log 或回滚卡死。
- 目标单次事务执行时间控制在 0.5–2 秒内,可用
SELECT UNIX_TIMESTAMP(NOW(3))记录起止毫秒来校准 - 有二级索引时,建议每批 2000–5000 行;纯主键删可放宽到 10000 行
- 删完加
DO SLEEP(0.1)缓解 I/O 压力,高负载时调到0.5
断点续删必须存 checkpoint,不能靠人肉查最后 id
存储过程中途挂了,没人能准确说出删到哪一行——下次重跑可能重复删或跳过数据。
- 建轻量表:
CREATE TABLE delete_checkpoint (table_name VARCHAR(64), max_id BIGINT UNSIGNED, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) - 每次删完更新
max_id为本批最大主键值(如SELECT MAX(id) FROM ...) - 下次启动先查
max_id,WHERE id > last_max_id AND ...续上,避免游标偏移或漏删
真正难的不是写完存储过程,而是判断哪张表该用主键游标、哪张该用时间字段、有没有外键或触发器拖慢速度、binlog 日志空间够不够撑住整个过程——这些细节不提前摸清,脚本一跑,DBA 就得半夜爬起来救火。











