删千万行数据必须分批、带索引、控节奏、可中断;where字段无索引会导致全表扫描、锁行超时;需用主键或时间字段游标,预估外键、触发器及binlog容量影响。

直接在存储过程中写 DELETE FROM table WHERE ... 删千万行,大概率会锁表、撑爆 binlog、拖垮主从同步,甚至被数据库主动 kill。必须分批、带索引、控节奏、可中断。
WHERE 条件字段没索引就别执行存储过程
哪怕加了 LIMIT,只要 WHERE 字段(比如 created_at 或 status)没索引,MySQL 每次都得全表扫描——不仅慢,还会锁住大量无关行,Lock wait timeout exceeded 是常态。
- 先跑
EXPLAIN SELECT * FROM your_table WHERE created_at ,确认 <code>type是range或更好,key显示用了哪个索引 - 没索引就加:
ALTER TABLE your_table ADD INDEX idx_created_at (created_at);线上加索引务必选低峰期,MySQL 5.6+ 可加ALGORITHM=INPLACE - 组合条件更常见,优先建联合索引,比如
(status, created_at, id),让ORDER BY id也能走索引
MySQL 存储过程里必须用 ROW_COUNT() 判断是否退出
靠 WHILE i 这种硬循环数批次,最后一批可能只删了 3 行,但循环还在跑,浪费资源还可能误删后续数据。
- 每次
DELETE ... LIMIT 1000后立刻跟SELECT ROW_COUNT() INTO deleted_rows -
IF deleted_rows = 0 THEN LEAVE loop_label; END IF;,这才是真实反馈 - MySQL 5.7+ 支持
LIMIT接变量,但 5.6 及更早不支持,别写LIMIT batch_size,老实用常量
每批删多少、隔多久,得看实际耗时,不是拍脑袋定
“每删 1000 行就 COMMIT”是典型误区:太频繁放大事务开销;太稀疏(比如 5 万行一提交)又容易卡死 undo log。
- 目标单次事务执行时间控制在 0.5–2 秒内,可用
SELECT UNIX_TIMESTAMP(NOW(3))记起止毫秒来校准 - 纯主键删可放宽到 10000 行/批;有二级索引建议 2000–5000 行/批
- 删完加
DO SLEEP(0.1)缓解 I/O 压力,高负载时调到0.5 - 别省略
COMMIT—— InnoDB 不提交就不会释放行锁和 undo 空间
断点续删必须存 checkpoint,不能靠人肉查最后 id
存储过程中途挂了(网络抖动、超时、DBA kill),没人能准确说出删到哪一行。下次重跑要么重复删、要么跳过数据。
- 建轻量表:
CREATE TABLE delete_checkpoint (table_name VARCHAR(64), max_id BIGINT UNSIGNED, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) - 每次删完更新:
UPDATE delete_checkpoint SET max_id = (SELECT MAX(id) FROM your_table WHERE id > last_max_id AND ...), updated_at = NOW() WHERE table_name = 'your_table' - 下次启动先查:
SELECT max_id FROM delete_checkpoint WHERE table_name = 'your_table',然后WHERE id > last_max_id AND ...续上
真正难的不是写出循环逻辑,而是提前判断这张表该用主键游标还是时间字段游标、有没有外键或触发器拖慢速度、binlog 日志空间够不够撑完整个过程——这些细节不摸清,存储过程写得再漂亮,上线就是事故。










