1. 为什么大批量 DELETE 是 MySQL 的隐形风险
1.1 一条 DELETE 背后到底做了什么
有一次在做数据清理时,开发同事直接在主库执行了这么一条语句:
DELETE FROM 订单流水表 WHERE create_time < '2023-01-01';
他以为这是一条普通的运维 SQL。执行不到五分钟,在线业务全部卡住,主库连接数打满,从库延迟直接飙到上千秒。最后这条 SQL 被 kill,但影响持续了半个多小时,后续积压的事务把 IO 拖了很久才恢复。
之所以一条 DELETE 能造成这么大的影响,是因为在 InnoDB 里删除数据并不是简单地把文件内容抹掉,它要做的事情比大多数人想象的多得多。
首先,引擎要根据 WHERE 条件找到所有需要删除的行。如果筛选条件上恰好没有合适的索引,那就是全表扫描;如果表有几千万行,这个扫描动作本身就够数据库喝一壶的。其次,每删除一行,InnoDB 都要去维护主键索引和该表所有相关的二级索引,一行可能涉及多个索引页的更新。再者,事务要把旧版本的记录写入 undo log,便于并发事务通过 MVCC 读到删除前的数据,还要把所有变更写入 redo log,用来保障崩溃恢复。最后,相关数据页上的行锁和记录锁,在事务未提交之前都不会释放。
可以简单理解成,你要求仓库里的一台叉车,一次性把所有角落的旧货清空。叉车走到哪里,通道就要封锁到哪里。你在导航上说“清空旧货”,但实际执行时,每个货架都要开门、清点、标记、登记。这个过程越长,其他要进仓库取货的作业员就越着急。
所以真正的问题从来不是“DELETE 本身会不会执行完”,而是它在执行期间产生的行锁、undo、binlog 和 IO 压力,会不会把同一台实例上的其他业务拖垮。明白了这一点,后面所有的方案选择就都有了依据:我们要做的,不是找到一条最快的 DELETE,而是把删除带来的影响控制在业务可接受的范围内。
1.2 哪些表最容易因为批量删除而出问题
并不是所有大表删除都会出问题。有的表一条 DELETE 跑完,影响也就几秒;有的表只删一小批,线上就哀鸿遍野。区别通常在于这几个特征:
- 单表体量巨大,比如总行数过亿,或者物理文件超过 100GB;
- 待删除数据占比高,比如一次要删掉总量 30% 以上;
- 表上二级索引多,三个起步,每个索引都要跟着主键记录同步更新;
- 业务实时读写频率高,同一时刻还有大量 INSERT、UPDATE 在抢资源;
- 主从架构采用 ROW 格式的 binlog,DELETE 会被放大成每一行的独立变更事件;
- 磁盘是机械盘或者 IO 能力本身不足,批量刷盘很容易把 IO 打满。
我见过最有代表性的场景是一个用户行为日志表,每天新增 5000 万行,按天滚动保留 30 天,也就是说每天要删掉 30 天前的 5000 万行。这种表用传统 DELETE 做肯定不行,正解是在一开始就设计成分区表,每天只需要 TRUNCATE 一个分区;如果当初没分区,后面改造涉及的锁表、复制数据成本非常大。
还有一种容易被忽略的场景:表本身不大,只有两三百万行,但业务要求删除其中 80% 的过期记录。这种看起来体量不大,执行 DELETE 时因为索引维护和锁竞争也可能非常慢。重点不在“绝对体量”,而在“删除数据占比”和“是否有可用索引”。
1.3 动手删除前,先花十分钟做数据体检
在我自己的流程里,不管接到什么删除需求,第一条都是先跑一组信息收集 SQL,把基础情况搞清楚再动手:
SELECT COUNT(*) FROM t1 WHERE 需要删除数据的过滤条件; SELECT MIN(create_time), MAX(create_time) FROM t1 WHERE 需要删除数据的过滤条件; SHOW INDEX FROM t1; SHOW TABLE STATUS LIKE 't1';
SELECT @@version, @@tx_isolation, @@binlog_format;
这几条命令能告诉你几件事:符合删除条件的数据到底有多少、对应的 id 范围在哪里、过滤条件上有没有索引、表的总数据量和物理大小是多少、实例版本和复制格式是什么。
我犯过的错误,就是在一次清理任务里没有先查看过滤字段上有没有索引,直接按 create_time 写了一条 DELETE,结果一个全表扫描把生产库拖了。后来再处理任何类似需求,我都会先确认过滤字段是否走索引。如果过滤条件本身没有索引,不管用哪种分批删除方案,每一批去定位待删除记录时都可能付出高昂的扫描代价,只能想办法绕开。
在数据情况摸清之后,还要做一件看起来多余但出事时救命的事:备份。一次性删除上千万行,谁也不能保证 WHERE 条件绝对准确。最稳妥是有一个可切换的从库或者最近一次的物理备份;如果实在没有条件,至少要把 binlog 保留策略确认好,真删错了还可以从 binlog 找回删除前的数据状态。
处理到这一步,删除本身还没开始,但你已经把大部分风险控制在自己手里了。接下来才是具体的删除方案。
2. 5 种批量删除方案实操拆解
2.1 方法一:分批 DELETE + LIMIT,最稳妥的通用打法
这是使用频率最高、适用面最广的方法,思路很简单:不一次性删除全部目标数据,而是以主键或唯一键为边界,一次删除一小批,提交后循环继续,直到删完为止。
最朴素的第一版写法长这样:
DELETE FROM t1 WHERE create_time < '2023-01-01' LIMIT 1000;
反复执行到影响行数为 0。优点是代码最短,缺点是如果 create_time 上没有高效索引,每次定位删除范围都可能在扫大表,后面的批次会越来越慢。更好的写法是用主键顺序分页:
DELETE FROM t1 WHERE create_time < '2023-01-01' AND id BETWEEN 1 AND 50000;
然后下一次把起点更新为 50001,继续执行相同的结构,直到 id 超过边界。也可以用 LIMIT 搭配 ORDER BY 主键:
DELETE FROM t1 WHERE create_time < '2023-01-01' ORDER BY id LIMIT 2000;
这种方式保证每一批都是从待删除集合的最小主键开始删,后续批次沿主键递增推进,磁盘 IO 相对局部化,事务大小也很好控制。
为了自动化,可以把固定批大小的删除封装成存储过程:
DELIMITER $$
CREATE PROCEDURE sp_batch_delete() BEGIN DECLARE v_rows INT DEFAULT 1; WHILE v_rows > 0 DO DELETE FROM t1 WHERE create_time < '2023-01-01' LIMIT 500; SELECT ROW_COUNT() INTO v_rows; COMMIT; SELECT SLEEP(0.5); END WHILE; END$$
DELIMITER ;
如果需要在过程里动态传批大小,DELETE 的 LIMIT 不能直接写参数,需要拼 SQL 后用 PREPARE/EXECUTE 动态执行,或者直接用固定常量。
有个细节值得特别强调:循环里别用 COUNT() 去判断“还要删多少行”。我见过同事在删除循环里写 SELECT COUNT() FROM t1 WHERE create_time < '2023-01-01';,每删一批就全表统计一次剩余量。原本删除本身没问题,结果被这个统计折腾得每轮都全表扫描。正确做法是用 ROW_COUNT() 拿当前 DELETE 实际影响的行数,只要还有行被删就继续,影响行数为 0 就退出。
分批 DELETE 另一个容易被忽略的点是 sleep。如果业务高峰期不能停机,或者主从架构对延迟比较敏感,每次循环之间最好 sleep 0.5 到 2 秒,给从库应用 binlog 留出追赶的时间,也避免 CPU 和 IO 一直被删除任务占住。压测时我一般先 sleep 0.5,观察主从延迟没有抬头再考虑收紧。
2.2 方法二:重建表,留下该留的数据而不是去删老数据
如果说分批 DELETE 是“把垃圾一点点搬出去”,那重建表就是“把有用的家具先挪到新房间,再把旧房间整体处理掉”。当你要删的数据比例非常高时,重建表比 DELETE 高效得多,因为它不用逐行更新大量索引,只需要把需要保留的数据重新写入一张新表。
常规步骤如下:
- 创建与旧表结构一致的新表:CREATE TABLE t1_new LIKE t1;
- 把需要保留的数据复制到新表:INSERT INTO t1_new SELECT * FROM t1 WHERE create_time >= '2023-01-01';
- 确认数据一致后换表:RENAME TABLE t1 TO t1_bak, t1_new TO t1;
- 确认新表运行稳定后,DROP TABLE t1_bak;
这个方案最大的优势是把“大批量删除”转化成“按需复制 + 瞬间改名”,在线影响窗口主要集中在新老表切换的那一刻。旧表被改名保留,数据并没有立即消失,等于多了一重保护,出问题还能切回去。
但缺点也不能忽视。新旧表同时存在,要求磁盘空间要装得下两份数据。比如原表 200GB,保留数据 50GB,执行期间峰值可能需要占用 250GB 以上空间。磁盘不足时这个方法不能直接用。另外,INSERT INTO ... SELECT 本质也是一次大查询加大写入,如果业务表还在持续写入,就需要停机窗口或者在应用层做双写,否则新表数据会与旧表不一致。
这个方案适合“表里 90% 都是过期数据、留下来只有很小一部分”的清理场景。一次导出 10 万行有用数据,远比重建那份仍在不断增长的大表合适得多。还有一点经验:如果保留数据也要几千万行,INSERT INTO ... SELECT 的时间会很长,建议分多批插入,或者先用 SELECT 导出到文件,再用导入工具加载。
2.3 方法三:分区表 + TRUNCATE PARTITION,规律归档的首选
前面两种方法都在“临时清理”的范畴里;如果删除需求是固定节奏发生的,比如日志表每天归档、流水表按月保留,那就应该认真考虑分区表方案。
分区表的思路不是把数据删掉,而是让数据本身物理落在不同的分区文件里。删除历史数据时,直接 TRUNCATE 对应分区,速度快到离谱:
ALTER TABLE t_log TRUNCATE PARTITION p202501;
TRUNCATE PARTITION 跟 DELETE 有本质区别。它不逐行解析、不更新二级索引,也不会产生海量 undo 日志,而是直接把这个分区的数据页整体废弃掉,代价相当于在存储层面做一次物理重置。执行过程几乎不影响其他分区上的读写,这对海量日志场景是真正可落地的方案。
分区的设计要提前做好,通常按日期范围是最常用的:
CREATE TABLE t_log ( id BIGINT NOT NULL AUTO_INCREMENT, content VARCHAR(500), create_time DATETIME, PRIMARY KEY (id, create_time) ) PARTITION BY RANGE (TO_DAYS(create_time)) ( PARTITION p202501 VALUES LESS THAN (TO_DAYS('2025-02-01')), PARTITION p202502 VALUES LESS THAN (TO_DAYS('2025-03-01')) );
这里有个关键点:分区键必须包含在主键或者唯一索引里。如果你把 id 设为主键、create_time 又作为分区键,会直接报错,因为 MySQL 要求每个唯一键都必须包含分区键。很多人第一次建分区表就是栽在这个地方。
如果已经存在的大表没有分区,临时改造也不是不可以,通过 ALTER TABLE ... PARTITION BY RANGE 实现,但这等同于重建表,会长时间锁资源和占用空间,需要安排在停机窗口做。每次删除旧分区还要提前创建好后续新分区,最好配合定时任务自动加分区。
TRUNCATE PARTITION 还有一个要注意的地方:它对数据的恢复几乎没有任何回旋余地,不做逐行 binlog 记录,也没有 undo 可以用来闪回。所以在执行前必须确认这部分数据后续不会产生查询、审计或恢复需求,或者已经做过归档备份。
2.4 方法四:临时范围表 / 归档表做冷热分离
有时我们既不想用大批 DELETE,又担心分区改造成本太大,可以考虑把“删除”变成“写出归档 + 不再查询”,也就是常说的冷热分离。
做法不复杂:先建一张结构相同的历史归档表 t1_archive,把需要删除的老数据定时 INSERT INTO t1_archive SELECT ... 迁移过去;原表 t1 中的数据继续留着,但业务上通过日期条件只访问最近的数据,让旧数据自然沉底。等确认归档完成、业务确实不再依赖在线表里的旧数据之后,再找低峰期通过分批 DELETE 把这些已归档数据清理掉。
这种做法的好处是删除动作被延后,真正影响在线业务的删除被放到了维护窗口,而且删除的范围可以通过归档进度精确控制。我遇到过不少团队采用这个方式之后,主库的负载曲线立刻平滑了很多。
如果不想保留这些过期数据,只想起到“缓冲”作用,也可以把待删除的主键范围先写入一张临时 id 表,后台脚本按这些 id 分批删除。这样就算 DELETE 过程中途失败,重新启动脚本也能从临时表里恢复任务进度。
比起裸写一条 DELETE,这种间接方案多几层步骤,但从可观测性和可恢复性上看,非常值得。
2.5 方法五:异步队列 + 后台消费,把删除变成可控调度
当删除任务量特别大、单次维护窗口内做不完,并且线上业务对资源占用非常敏感时,可以考虑把删除任务异步化。核心思想是:不在触发删除时直接执行大 SQL,而是把删除目标写进队列,由后台消费者按照可控速率一批批处理。
实现上不一定要引入重量级消息中间件。我见过很实用的做法是建一张 task_ids 表,把所有待删除行的主键 id 分批插入,然后后台脚本按 id 范围循环删除。删除速率可以通过消费者线程数、每次批大小、批次间隔动态调节,也可以根据主从延迟指标自动暂停或加长 sleep,做到“让删除尽量隐形”。
这个方案比较适合超大规模历史数据清理,因为它天然具备断点续跑的容错能力。任务中断了,重启消费者继续消费即可;删除出现异常了,可以随时暂停。不过它的代价是实现复杂度更高,相当于为了一次删除维护一个小型任务系统,需要业务方明确评估收益。
我自己倾向把“异步队列 + 后台消费”当作最后手段,只有前几种方法都被验证过、确实不满足时才上;如果只是几千几万行的清理,用这种方案反而属于过度设计。
3. 批次大小和性能参数怎么定
3.1 单批删除行数怎么选才合理
很多人拿到方案后第一个问题就是:一批删多少行合适?这个没有万能答案,但可以通过两个指标科学估算:一是单批数据量,二是单批执行时长。
先看单批数据量。假设表中一行记录平均大约 500 字节,5000 行一批就是 2.5MB 的 IO 操作,SSD 上处理这种量级很轻松,机械盘要谨慎一些。如果单行包含多个大文本字段、平均 5KB,那一批 5000 行就是 25MB,这个量级就不小了,应该把批大小降到 1000 行甚至 500 行。
再看单批执行时长。理想情况下,每一批 DELETE 耗时应该控制在 1 到 3 秒以内。如果某批超过 5 秒,说明批太大或者定位条件有问题。我通常的做法是先拿 500 行做首个批次,观察耗时,然后等比放大到 1000、2000、5000,记录每批耗时和主从延迟,选择耗时增长变陡之前那一档来使用。
此外还要考虑实例配置。如果机器 IO 本身不行,或者同一实例上还跑着其他核心业务,批大小就往小了选。删除任务没有绩效考核,稳定排第一位,宁可多跑几轮也别把服务器压垮。
3.2 单条 DELETE 的细节优化
性能优化能做的点很多,这里挑几个踩过坑后沉淀下来的经验。
第一,能按主键范围删就别按普通索引条件删。在 InnoDB 里,主键是聚簇索引,数据行本身按主键顺序组织,按主键范围删除可以顺序扫描数据页,效率远高于通过二级索引定位再回表删除。如果删除条件是从 create_time 来的,建议先查一次 MIN(id) 和 MAX(id) 确定边界,然后转换成主键范围分批执行。
第二,每批处理完立即 COMMIT。大批量删除一定要拆事务,否则 undo log 会无限膨胀,锁的保有时间也会被拉得很长。每次删除的批大小固定,事务也尽量固定在一个可控范围内,一提交就释放一批锁和日志空间。
第三,要关注 binlog 格式。如果实例使用 ROW 格式,DELETE 每删一行都会在 binlog 里记录完整的行前后映像,删除 1000 万行就会产生极其可观的 binlog。从库回放这些事件时同样要执行大量行操作。在这种配置下,更要把批大小放小一些,同时监控从库延迟。如果业务允许,把 binlog 格式调整为 MIXED 或 STATEMENT,binlog 体积会小很多,但必须做安全评估,确认不会影响主从一致性。
第四,删除前检查索引是否必要。二级索引在 DELETE 时也要同步更新,如果一个索引在删除场景里用不到,但又长期存在,每次清理都会付出额外代价。不要为了偶发的查询需求保留一个价值不高的索引。当然,删索引本身也要谨慎,最好通过慢查询日志验证确实用不到再删。
3.3 主从延迟和复制格式的取舍
主从架构下,主库的 DELETE 最终会在从库回放。如果从库是异步复制,主库大批删除时从库跟不上,就会造成读取延迟。严重的时候,从库上会堆积大量复制任务,监控画面上 Seconds_Behind_Master 会非常吓人。
控制主从延迟的核心仍然是批次大小和删除节奏。我惯用的做法是在删除循环里加一个延迟观察:每跑完一批,查询一下从库的 Seconds_Behind_Master,超过自己设定的阈值比如 300 秒时,sleep 时间自动拉长,低于阈值时再恢复原节奏。这个逻辑在存储过程里写起来有些繁琐,放在外部脚本里更灵活。
还要区分 binlog 格式对从库的影响。ROW 格式下从库执行 DELETE 需要一行一行删除,主库删 100 万行,从库也要删 100 万行,延迟积累自然明显;STATEMENT 格式下 binlog 只记录一条 DELETE 语句,从库回放时相当于重放一个大批量删除,同样会有长时间锁定和资源占用。所以无论哪种格式,只要是大批量删除,主从延迟都躲不开,能做的主要是控制节奏和错峰。
有条件的场景,可以考虑把删除任务放在一个独立的从库上执行,或者在删除期间让部分非关键查询切到其他副本,保障核心线上查询不受影响。这些都需要提前和业务方对齐,不是 DBA 单方面能决定的。
4. 实战中踩过的坑和排查方法
4.1 锁等待超时,先查长事务而不是急着杀 DELETE
报错 ERROR 1205: Lock wait timeout exceeded 是批量删除场景里最常见的错误之一。乍一看像是 DELETE 自己出了问题,但大部分情况下真正的问题根源不在 DELETE,而在于有另一个事务长时间持锁没提交。
排查第一步,用这两条 SQL 看当前事务和锁等待:
SELECT * FROM information_schema.innodb_trx\G; SELECT * FROM information_schema.innodb_lock_waits\G;
你会发现 DELETE 在等某个事务持有的锁释放。如果这个事务是个跑了很久的未提交写事务,甚至是一个不知道哪来的空闲连接还开着事务不提交,推荐先把那个长事务 kill,再重试 DELETE。
曾经碰到一种情况是 DELETE 一直卡着,查了半天发现是一个应用连接执行了 SELECT 之后没有关闭事务,还持有表上的元数据锁。DELETE 想拿到表结构相关的锁,结果一直排队。这种问题不通过锁查询根本看不出来。
再补充一个防坑技巧:如果线上要跑批量删除,可以先把会话里的 innodb_lock_wait_timeout 调小一些,比如 5 秒,避免 DELETE 无限等待下去。
SET SESSION innodb_lock_wait_timeout = 5;
超时之后脚本报错,至少我们知道这里有并发事务的存在,而不是让任务一直挂在那里拖一个晚上。
4.2 删完之后磁盘空间没变小,这不是系统坏了
经常有人在跑完大批量 DELETE 后发现表空间文件还是一样大,以为删除没生效,或者怀疑 MySQL 有 bug。其实这是 InnoDB 的预期行为。
普通 DELETE 只是把数据页里的记录标记成已删除状态,并把空间标记为可复用,文件本身不会立即缩小。InnoDB 在后台通过 purge 线程清理由删除产生的历史版本,让那些页重新可用于后续插入。如果后续又有新数据写入,这些空间会被复用;但如果删除后没有任何写入,文件大小保持不变是正常的。
如果磁盘水位确实紧张,需要重建表来回收空间:
OPTIMIZE TABLE t1;
或者执行:
ALTER TABLE t1 ENGINE=InnoDB;
这两者本质都是重建表,能够把表压缩紧凑,同时整理碎片。但要注意它们都有代价:需要额外磁盘空间,执行期间对表有较重的锁影响,大表上耗时要以小时计。所以一定要安排在维护窗口做,并且在执行前确认磁盘剩余空间足够容纳新表。
另一个容易被忽略的点是,大批 DELETE 期间 binlog 和 undo 都会飞速膨胀。有时候你看到磁盘没降反而升,其实不是表空间的问题,而是 binlog 文件没来得及清理,或者有一个长事务导致 undo 一直没被 purge。处理思路是先排查大事务是否还存在,再把 binlog 保留时间和清理策略做好。
4.3 主从复制中断,怎么恢复和自查
批量删除在从库上回放时,如果与业务写入的时序产生交集,偶尔会出现复制中断。典型的报错是 SQL 线程报 1032 或 1062:要么是主库删除的那一行在从库上找不到了,要么是从库上插入时主键冲突。
遇到这种问题,第一步永远是看一下复制状态:
SHOW SLAVE STATUS\G;
重点看 Last_SQL_Errno 和 Last_SQL_Error。根据错误类型选择处理方式:如果是 1032,确认从库上是不是因为某种原因已经缺失对应行,可以跳过该错误继续复制,也可以手动补一条 DELETE 再继续;如果是 1062,通常说明主从数据之前已经出现不一致,需要重新对齐数据。
这里强调一个经验:批量删除任务放到低峰期执行,而且批次之间的 sleep 不能省,能大幅降低与业务写入发生交叉的概率。另一个更保险的做法是,删除前先锁定相关业务写入,或者把删除任务放到业务只读的副本上,跑完再切换回主库。
复制中断的恢复没有统一万能命令,关键是要先理解不一致产生的原因再去处理。如果只是跳过错误,后续可能继续累积更多不一致数据,最后只能重新做数据校验和修复。
5. 方案怎么选:一张对照表加一点判断逻辑
5.1 三个问题决定删除方案
在实际项目里做选择,我的判断顺序大概是:先问这次删除是什么频率,一次性还是重复发生;再问能接受多长的停机窗口,是完全不能停还是可以安排维护;最后问删除的数据后续有没有恢复可能性。这三个问题定下来,方案基本就清晰了。
下面这张表是我常用的对照参考:
| 场景 | 首选方案 | 原因 |
|---|---|---|
| 几百万行以内、一次性清理 | 分批 DELETE + 主键范围 | 改造最小,效果可预测 |
| 表很大、保留数据少 | 重建表 / 导出导入 | 避免大量索引维护 |
| 长期规律性归档 | 分区表 + TRUNCATE PARTITION | 删除维护成本最低 |
| 业务无法停机、需要限流 | 异步队列 + 后台消费 | 可暂停、可恢复 |
| 数据后续需要审计 | 先归档再做清理 | 防止误删无法恢复 |
如果只是一次性清理,我优先分批 DELETE;如果是每天都要清历史日志,我建议哪怕费点劲也要改造成分区表。如果删除之后马上有一大批新数据要写进来,可以用重建表或者配合后续 OPTIMIZE,让数据页更紧凑,空间复用也更好。
5.2 删除完成后的收尾工作同样不能省
大批量删除跑完之后,很多人以为万事大吉,其实还有两件事值得做。
第一是空间回收与碎片整理。如果删除的数据量非常大,表空间碎片会很多,查询性能和后续写入都可能受影响。需要在业务低峰期执行一次 OPTIMIZE TABLE 或 ALTER TABLE ... ENGINE=InnoDB,把表重建一遍。执行前确认磁盘剩余空间,并估算锁表时间,最好提前跟业务同步维护窗口。
第二是执行记录和效果复盘。把删除任务的总耗时、总行数、binlog 增量、主从延迟峰值、磁盘空间变化记录下来。下次再遇到类似需求时,这些数据能让批次大小和 sleep 时间的调整有据可依,而不是每次拍脑袋。
删除任务如果比较大,最好单独开一个低峰期模块去跑,或者在独立从库上跑,比在主库上硬扛要稳得多。删除不是越快越好,而是越稳越好。
6. 最后再分享一点个人体会
做数据库维护这些年,我最大的感受是:大批量删除之所以事故频发,不是因为它技术上有多难,而是因为很多人在执行之前完全没意识到一条 DELETE 背后牵动的是锁、日志、复制、IO 一整条链路。
我现在处理任何删除任务,必经的流程是先看数据和索引情况,再选删除策略,然后用小批次试探性能和延迟,最后全程盯监控。这套流程看起来笨拙,但每次都能安全落地。曾经因为跳过试探步骤直接删除,我在深夜处理过一次恢复任务,之后就再也没跳过。
还有一个小技巧:无论用哪种删除方式,执行第一条语句之前,先在测试环境或者备库上把同样的 SQL 跑一遍,观察执行计划和耗时。这一步花不了多长时间,但能提前发现过滤条件有没有走索引、批大小是否合理、会不会锁表,很多生产事故本来是可以避免的。
最后建议大家把删除任务的执行代码和参数写进自动化脚本,别用手工 SQL 裸跑。手工操作容易忘掉 COMMIT、容易忘掉 sleep,也容易误改 WHERE 条件。自动化脚本虽然前期多花十几分钟,但换来的是可重复、可暂停、可回看的整套操作记录,对维护长远的数据库健康非常有帮助。