
做 MySQL 的人早晚都会碰上这么一件事线上表里堆了上亿行旧数据磁盘快报警了产品让你把两年前的记录全部清掉。听起来无非是一条 DELETE 的事但你要是真敢在生产环境直接执行DELETE FROM big_table WHERE create_time 2022-01-01大概率会在半小时内收到一堆复制延迟、锁等待、连接打满的告警。这不是 SQL 写法问题而是 MySQL 在处理海量数据删除时代价远比你想象中高。这篇文章我直接把这几年处理 MySQL 批量删除海量数据的几种常见方案摊开讲从“为什么不能一条 DELETE 删到底”这类底层原理到分批删除、重建表、分区表、pt-archiver 工具化归档等实操手段再到删除之后的表空间回收和主从同步检查。看完你至少能针对自己的场景选出一种方案并且知道每一步为什么这么做、坑又在哪里。1. 先把问题看清楚为什么海量数据删除这么难不少人对 DELETE 的理解还停留在“把行从表里划掉”。实际上在 InnoDB 引擎里删除一行背后要做的事远不止标记一个位置它涉及索引树维护、undo 日志、redo 日志、binlog 同步、后台 purge 线程清理等一系列动作。如果删除量达到千万行甚至上亿行连锁反应就会被无限放大。1.1 InnoDB 删除一行远不止“划掉”这么简单InnoDB 是索引组织表聚簇索引的叶子节点直接存放整行数据。执行 DELETE 时MySQL 首先要在聚簇索引上找到这一行并给它加锁如果有二级索引引用这行二级索引里的记录也要同步处理。光是找行和加锁这两件事就决定了“大范围删除”必然和高锁竞争绑定。删完行之后数据并不会立刻被物理抹掉。InnoDB 会把这行标记为已删除真正清理工作交给后台 purge 线程而且删除动作要写 undo log。为什么写 undo log万一事务回滚或者有其他事务在更早的快照里需要读到这行InnoDB 得靠 undo log 把旧版本捞回来。这就是长事务的致命点删除的事务一直不提交undo log 就一直膨胀磁盘占用越来越大其他事务的可见性判断也会受影响数据库性能随之下降。另一个容易被忽略的是 binlog。主从复制环境下主库每执行一条 DELETE从库会通过 binlog 重放同样的删除操作。如果你一次 DELETE 删了五千万行binlog 里就有大量行级事件从库要一行一行重放哪怕主库删完了从库可能还在追主从延迟就是这么被拉爆的。这种现象在 binlog_format 是 row 的模式下尤其明显。所以海量删除一旦没规划好不只是主库卡整个集群都会被拖下水。1.2 一条大 DELETE 常见的三个致命连锁反应第一个反应是事务太大undo 和锁不容易收场。MySQL 的 DELETE 在 InnoDB 里是一个事务默认 autocommit 开启时一条 DELETE 如果中途失败会作为整体回滚如果执行成功则到语句结束才提交。大事务意味着锁持有时间极长期间任何对该区域的读写请求都会排队等锁表现就是大量会话处于Waiting for lock状态连接数被打满应用侧超时报错。第二个反应是主从复制延迟。前面说过row 格式的 binlog 会把每行删除都记录下来从库重放这些事件的时间可能远远长于主库。尤其是在从库硬件不如主库、或者主库并发压力大的场景延迟可能从几秒滚到几小时。主从延迟一旦堆积业务侧的读写分离策略就会出大问题从库查不到刚刚删除的数据或者数据明显滞后。第三个反应是“空间其实没有真正释放”。InnoDB 删除大量行后表空间文件大小基本不会立即缩小。那些被删掉的记录空出来的页只是被标记为可复用后续有新数据写入时会去填充但如果你想要磁盘立刻瘦身delete 做不到还得依赖 OPTIMIZE TABLE 或重建表。很多人删完几千万行以为磁盘会空出几个 GB结果ls -lh一看还是原来的大小这就是没搞懂 InnoDB 的页管理逻辑。2. 方案一最基础的按批 DELETE重写成游标式分段删除讲方案之前先给一个结论海量删除几乎不支持“一条 SQL 删到底”除非你想把作业停掉。实际生产中最稳妥的下手点就是把大事务拆成无数个小事务每批只删一小段删完立刻提交让锁间隙和 binlog 重放压力都控制在可控范围内。这就是按批 DELETE 的基本思路别小看它很多千万级场景靠这一招就够用。2.1 分批删除的通用原则把一次大事务拆成 N 个小事务分批删除的核心是什么不是单纯把 DELETE 后面加个 LIMIT而是控制“一个事务处理多少行、锁多久、产生多少 binlog”。每删一批数据就提交一次InnoDB 的锁随之释放从库也能在事务边界处喘口气即使删除过程中出现死锁或超时最多回滚这一小批不用整表从头再来。这里有一个非常常见的误解有人觉得DELETE FROM t WHERE create_time 2022-01-01 LIMIT 1000跑了就算分批但实际上并不稳妥。如果 create_time 上没有合适的索引MySQL 每次都可能从头开始扫描最后一行一行匹配效率极低还可能误删同一次扫描中前面的记录。真正可靠的分批条件应该是基于主键或唯一索引的区间因为主键访问不需要全表扫描而且每次锁定的边界非常明确。一个简单的通用原则删除条件可以复杂但每一批的“划界字段”必须走主键或唯一索引每次只处理一个主键区间每批行数控制在百到千这个量级批与批之间加上 sleep给主库和从库缓冲时间。这些原则在下面的 SQL 示例里都会体现。2.2 按主键范围分段删除直接写你会用的 SQL假设有一张流水表t_log主键为自增id准备删除id小于 1 亿且create_time早于 2022 年的数据。可以先查一下目标范围然后按主键窗口切分比如每次删 2000 行。第一步确定范围SELECT MIN(id), MAX(id) FROM t_log WHERE create_time 2022-01-01;第二步按窗口循环删除。这里可以直接手写一个存储过程也可以在外面用 Python、Shell 循环拼 DELETE 语句。手写存储过程的好处是逻辑固化我给出一个可用的模板DELIMITER $$ CREATE PROCEDURE sp_batch_delete(IN v_start_id BIGINT, IN v_end_id BIGINT, IN v_step INT) BEGIN DECLARE v_cur_id BIGINT DEFAULT v_start_id; WHILE v_cur_id v_end_id DO DELETE FROM t_log WHERE id v_cur_id AND id v_cur_id v_step AND create_time 2022-01-01; COMMIT; SET v_cur_id : v_cur_id v_step; DO SLEEP(0.1); END WHILE; END$$ DELIMITER ;调用时直接传参CALL sp_batch_delete(0, 100000000, 2000);。每次窗口范围只有 2000 个 id 值即使这些 id 之间有空洞最多也就扫一点范围锁的粒度不会无限扩大。注意这里的 WHERE 条件保留了create_time 2022-01-01防止把窗口内其他不该删的新数据误删。为什么要加DO SLEEP(0.1)不要小看这 100 毫秒。删除操作涉及磁盘随机 IO、binlog 写入、从库同步如果主库每批瞬间删完下一批又接着来压力是连续的加了短暂睡眠主库和从库都能获得喘息机会。实际生产里如果从库配置比较弱我会把 sleep 调大到 0.3 甚至 1 秒。2.3 更稳的写法按主键游标“取一页删一页”主键区间分段有个前提你要能先用MIN/MAX估算出范围。如果删除条件里的过滤字段和主键相关性很差或者 id 范围跨度过大区间循环会扫很多无用的页。这时候我推荐游标式写法每次先按条件取出一页目标主键再基于这批主键删除循环往复。以脚本方式举例假设你在 Python 或 Shell 里循环执行核心就是两条 SQL-- 第一步取一页目标主键用游标思想 SELECT id FROM t_log WHERE create_time 2022-01-01 AND id :last_id ORDER BY id LIMIT 2000; -- 第二步基于拿到的 id 列表批量删除 DELETE FROM t_log WHERE id IN (:id1, :id2, ...);为什么先查主键再删而不是直接DELETE ... WHERE create_time ... LIMIT 2000因为直接 LIMIT 的 DELETE 依赖优化器选择索引如果走全表扫描MySQL 要不断“从头开始扫到第一个匹配行再删”下一批又从头开始性能会非常难看。而先按主键排序取 id 列表查询稳定走主键索引拿到的是精确边界再删就轻松了。这种写法的关键变量有两个:last_id和每页大小。每页 2000 行是我个人常用的值行较宽的表可以降到 500行很窄的小记录表可以提到 5000。总的原则是单批事务的执行时间尽量控制在几秒以内宁可多跑几轮也不要让单次事务拖到十几秒。2.4 分批删除的调优和监控要点实战中我把几个调优点排个序重要性从高到低基本是这样第一先确认删除条件能走索引。如果不是拿主键当划界字段那至少要在业务过滤字段上建二级索引。曾经有个同事在 3 亿行的订单表上直接跑DELETE WHERE statusEXPIREDstatus 字段只有索引但区分度太低优化器直接放弃索引走了全表扫删了半小时发现几乎没进展就是这个原因。第二单批行数不要拍脑袋定。行宽 200 字节的小表2000 一批很合理行宽有两三 KB 的大表200 一批都嫌多。因为每批产生的 undo 和 binlog 是按行大小累加的不是按行数。第三盯着SHOW PROCESSLIST、performance_schema.data_lock_waits和主从延迟指标。分批删除很可能会出现部分批次被锁等待卡住你要能及时发现哪一批出了问题如果发现Threads_running持续偏高就把 sleep 调大或者降低单批行数。真有条件的可以在业务低谷窗口用计划任务执行并且拦住其他批量任务同时跑避免多个大事务并发。3. 方案二全表重建加原子 RENAME适合“留小删大”分批 DELETE 虽然通用但有个先天短板即便你用最优游标删几千万行的逐行删除依然耗时漫长。如果待删除数据占全表比例极大比如 3 亿行只留 3000 万行那“把要保留的数据拷贝到新表然后换表名”就比“逐行删旧数据”划算得多。这就是重建表方案数据库圈里管这种操作叫 “影子表替换”。3.1 什么时候该用重建表方案而不是硬删我判断是否用重建表通常看两个条件一是删除比例是否超过 70%二是能不能接受短暂的应用写入中断或者可以设计双写。删除比例越大重建表的优势越明显因为你要保留的数据量小拷贝成本低而逐行 DELETE 无论如何都要处理那 2 亿多行垃圾数据的索引维护和 binlog成本没法省。第二个条件容易被忽略。如果你在上班高峰跑重建表新表拷贝数据那段时间老表还在持续接收新写入最后 RENAME 的瞬间新表数据会缺一部分等于丢数据。所以这个方案落地前一定要跟业务方确认是选择在凌晨低峰期停写一小段时间还是通过双写机制保证新表数据完整。如果业务完全不允许停机那重建表方案就要很谨慎可以用 gh-ost、pt-online-schema-change 这类在线表结构变更工具它们在拷贝数据的同时通过 binlog 同步增量把切换过程对业务的影响降到最低。不过工具不是今天的主角下面重点讲手动作法。3.2 手把手操作建新表、拷数据、RENAME 三步走整个过程可以拆成四步我先给 SQL 骨架再逐条解释-- 第一步创建同构新表。注意索引、自增、字符集、行格式都要和原表对齐 CREATE TABLE t_log_new LIKE t_log; -- 第二步把需要保留的数据搬到新表 INSERT INTO t_log_new (col1, col2, ...) SELECT col1, col2, ... FROM t_log WHERE create_time 2022-01-01; -- 第三步原子切换表名 RENAME TABLE t_log TO t_log_bak, t_log_new TO t_log; -- 第四步确认数据量、索引、自增没有问题后再删旧表 DROP TABLE t_log_bak;CREATE TABLE t_log_new LIKE t_log会复制原表的列定义、索引、自增属性等表定义速度快而且不容易漏字段。但要注意外键、触发器这类对象不会被完整带过去如果原表有外键约束或者触发器需要你手动确认是否需要在新表上重建。第二步的 INSERT SELECT 是耗时大头。保留数据量如果也比较大比如几千万行不建议一条 INSERT 从头搬到尾建议也分页插入或者干脆先在应用层分批 SELECT 再插入。实际操作中我给一条参考保留 3000 万行、单行 300 字节在普通 SSD 上大概要跑二十分钟到一小时具体取决于目标库负载。第三步的 RENAME 是整套流程的精髓。MySQL 的 RENAME TABLE 是原子操作切换瞬间其他会话要么看到旧表名要么看到新表名不会出现中间态也不会复制数据。正因如此只要第一步和第二步准备好切换对业务的影响只有极短暂的元数据锁等待通常就是几十毫秒级。3.3 重建表方案最容易踩的五个坑第一个坑是空间不足。新表在拷贝数据期间老表还占着原来的磁盘空间这意味着你需要额外准备一块“保留数据大小 所有索引”的磁盘空间。曾见过有人没算清楚跑到一半磁盘 100%数据库直接进入只读状态相当被动。所以动手之前先统计好表大小、预计保留量和 mysqld 数据目录剩余空间。第二个坑是自增值丢失或者错位。如果用 INSERT SELECT 搬数据新表的 AUTO_INCREMENT 不会自动等于原表当前值。如果你原表主键是自增 id业务还在持续写入切换后可能出现主键冲突或者自增乱序。建议切换前查询SHOW TABLE STATUS LIKE t_log_new确认 Auto_increment 值必要时用ALTER TABLE t_log_new AUTO_INCREMENTxxx修正。第三个坑是外键和触发器。RENAME 之后原表名 t_log 上的外键关系、触发器不会自动迁移到新表。如果子表还引用着t_log这个表名RENAME 可能触发数据库自动修改外键定义也可能在新表上找不到对应外键导致后续写入报错。这一块很容易验证切换前用SHOW CREATE TABLE仔细比对。第四个坑是切换后索引统计信息是旧的。INSERT SELECT 插入大量数据后新表的统计信息概率不准后续查询可能走错执行计划。切完第一件事就是ANALYZE TABLE t_log_new;让优化器重新收集统计信息。第五个坑是旧表别急着 DROP。很多团队切完 RENAME 就立刻 DROP 旧表如果新表数据量核对没做或业务发现异常想回滚都没地方回。我的习惯是保留旧表至少 24 小时确认线上查询和写入稳定后再删。磁盘很紧张的话至少保留一天否则出了问题只能哭。4. 方案三分区表加分区级 DROP/TRUNCATE日志表清理的终极大招前面两种方案基本属于“事后补救”如果表已经建成大表了再想去清理。而如果你负责的表从一开始就有明确的周期规律比如日志表、流水表、历史消息表那最优雅的做法就是从建表阶段就做成分区表按月分区。海量数据删除这个问题在分区表面前会变得异常简单不再需要逐行 DELETE而是直接把整个分区丢掉。4.1 分区表为什么比 DELETE 快那么多分区表的清理操作是ALTER TABLE ... DROP PARTITION或ALTER TABLE ... TRUNCATE PARTITION。DROP PARTITION 属于表结构层面的操作它直接删除整个分区对应的数据文件和索引段不逐行遍历、不逐行加锁速度基本和你删除一个文件一样快。TRUNCATE PARTITION 的效果类似也是清空整个分区但保留分区结构。更关键的是 binlog 差异。普通 DELETE 在 row 模式下会产生海量行级 binlog 事件从库要费力重放而 DROP PARTITION 作为 DDL 记录从库直接执行一条 DDL 也能快速完成复制压力很小。这也是为什么同样删 1 亿行数据分区表只需要几秒普通表可能要跑几个小时。当然分区不是没有代价查询如果条件没带分区键很可能会扫多个分区性能反而不如单表。所以分区键的选择要跟业务查询特征强绑定通常就是日期字段因为绝大多数日志类查询都带时间范围。4.2 如何把现有普通表改造为分区表如果你有一张普通表想改成分区表有两种路径。一种是新建一张分区表把旧数据迁移过去然后 RENAME 切换这也是重建表方案的变体。另一种是直接用ALTER TABLE ... PARTITION BY语句在线改造但 MySQL 会在后台重建整个表耗时取决于表大小而且要求分区键必须包含在表的所有主键和唯一索引中。我用按月分区举个例子先看建表语句应该怎么设计CREATE TABLE t_log ( id BIGINT NOT NULL AUTO_INCREMENT, log_time DATETIME NOT NULL, device_id VARCHAR(32), message TEXT, PRIMARY KEY (id, log_time), KEY idx_device_id (device_id), KEY idx_log_time (log_time) ) ENGINE InnoDB PARTITION BY RANGE (TO_DAYS(log_time)) ( PARTITION p202301 VALUES LESS THAN (TO_DAYS(2023-02-01)), PARTITION p202302 VALUES LESS THAN (TO_DAYS(2023-03-01)), PARTITION p202303 VALUES LESS THAN (TO_DAYS(2023-04-01)), PARTITION p202304 VALUES LESS THAN (TO_DAYS(2023-05-01)) );这里有几个关键点。主键必须包含分区键log_time所以我把主键定义成(id, log_time)这是 MySQL 分区表的硬性规则TO_DAYS(log_time)把日期转成天数作为分区边界这样每个分区就是自然月的数据未来如果数据量涨了随时可以新增分区。如果你已经有一张现成的普通表临时想改成按月分区可以执行ALTER TABLE t_log PARTITION BY RANGE (TO_DAYS(log_time)) ( PARTITION p202301 VALUES LESS THAN (TO_DAYS(2023-02-01)), PARTITION p202302 VALUES LESS THAN (TO_DAYS(2023-03-01)), PARTITION p202303 VALUES LESS THAN (TO_DAYS(2023-04-01)), PARTITION p202304 VALUES LESS THAN (TO_DAYS(2023-05-01)) );注意这条 SQL 会重建整个表原表数据会按分区规则重新分布期间会有 IO 和锁压力不建议在业务高峰执行。如果表特别大还是优先走“建新分区表 数据迁移 RENAME”的三步法主动权在自己手里。4.3 分区表的日常清理和进阶玩法分区表建好之后日常清理就是一条 DDL 的事-- 清空 2023 年 1 月分区保留分区结构 ALTER TABLE t_log TRUNCATE PARTITION p202301; -- 直接删除 2023 年 1 月分区连同结构一起删掉 ALTER TABLE t_log DROP PARTITION p202301;我一般建议用 DROP PARTITION 而不是 TRUNCATE PARTITION。因为每个月的数据删掉后分区留着也是个空壳不如直接删掉再新建下个月的分区。在定时任务里可以每个月一号执行先 DROP 掉 N 个月之前的旧分区再 MODIFY/ADD 未来 N 个月的新分区ALTER TABLE t_log DROP PARTITION p202301; ALTER TABLE t_log ADD PARTITION ( PARTITION p202401 VALUES LESS THAN (TO_DAYS(2024-02-01)) );这里有个执行细节DROP PARTITION 之前最好用information_schema.PARTITIONS确认分区名和数据量。如果某些数据有审计要求不能直接删还有一种进阶玩法叫 EXCHANGE PARTITION把某个分区直接交换成一张独立普通表然后对这张独立表做归档、导出或者冷备等确认不需要了再删。示例ALTER TABLE t_log EXCHANGE PARTITION p202301 WITH TABLE t_log_202301;这条命令几乎瞬间完成因为底层只是修改数据字典中的映射关系并不是物理移动数据。交换之后再处理单独的表自由度就大多了可以用来做归档文件、导入数仓或者确认后 DROP。分区表方案唯一的门槛是“没后悔药”——如果你一开始没设计分区键后面改造的成本和你重建表差不多。所以我的建议很简单凡是明确有日期维度且会周期性增长的表建表第一天就上分区。5. 方案四pt-archiver 这类工具的取舍与实战如果你不想自己写存储过程也不想为每天都要跑的清理任务写一堆脚本Percona Toolkit 里的 pt-archiver 是个非常适合的现成工具。它专门用于“逐步归档删除 MySQL 大量数据”核心逻辑就是之前讲的游标分批但把限速、批量提交、从库延迟感知这些细节都封装好了。5.1 pt-archiver 的工作原理和基本用法pt-archiver 会按照你在--where里指定的条件通过主键或唯一索引逐条扫描数据每次获取一小批默认几百行在事务中处理完就提交。它和手工分批 DELETE 的区别在于工具自动控制每次 SELECT 的行数、事务大小、批量间隔还能感知主从延迟延迟高了自动暂停不至于把从库拖垮。最常用的删除场景命令长这样pt-archiver \ --source h127.0.0.1,P3306,uadmin,ppassword,Dtest,tt_log \ --where log_time 2022-01-01 \ --limit 100 \ --txn-size 100 \ --sleep 0.5 \ --purge \ --no-check-charset参数解释一下--purge表示只从源表删除不做归档不想要--purge的话可以加--dest指定归档表把数据同步到另一张表这就从“删除”变成了“归档”--limit 100表示每次 SELECT 拿 100 行--txn-size 100表示每攒够 100 行就提交一个事务--sleep 0.5表示每批操作之后睡 0.5 秒。综合下来这个工具每秒大概能删一两百行看起来不快但贵在稳主库和从库都不会被打冒烟。5.2 值得优先记住的调整参数用 pt-archiver 不要上来照抄命令先理解几个关键参数怎么配合。--limit和--txn-size不是一回事。--limit管的是每次 select 扫描的步长影响工具执行效率--txn-size管的是一个事务内包含多少操作影响事务大小和锁粒度。如果你想让工具更激进可以把两个值都调大比如--limit 500 --txn-size 500如果想更温柔那就都调小。--max-lag是防止从库延迟爆炸的保险丝。当从库的 Seconds_Behind_Master 超过设定值时工具会暂停执行等待从库追上再继续。这个参数我几乎每次都会加--max-lag5--statistics可以输出执行统计包括每秒处理行数、耗时、错误数。跑完一次清理任务务必开着统计看一眼很多诡异的问题都在统计里暴露出来。5.3 工具使用中常见的报错和避坑经验pt-archiver 最常见的报错是字符集不匹配。如果源表字符集和工具连接默认字符集不同很容易报 Illegal mix of collations 之类的错误。简单粗暴的方式是加--no-check-charset但根源还是建议在--source连接串里写清楚字符集比如加个--charsetutf8mb4。第二个坑是--where条件不走索引。pt-archiver 虽然封装到位但本质上还是 SELECT DELETEwhere 条件如果没有索引支撑它每批都要全表扫描速度会慢到让人怀疑人生。工具本身有校验提示但不会替你做优化所以用之前先用EXPLAIN确认一下。第三个坑是大表上不要乱加--bulk-delete。--bulk-delete会用一条 DELETE 删除一批所有匹配行速度更快但它会绕过事务逐行处理失去精细控制锁也会放大。日常后台清理我更倾向默认方式除非你明确知道表并发不高。有一点也必须提醒pt-archiver 的--purge模式不会回收表空间删除完之后表大小还是老样子。它解决的只是“稳定删除”后续的物理空间回收还是要回到下一章讲的收尾动作。6. 删除完成之后这些收尾动作不能省很多人以为 DELETE 跑完就大功告成实际上“删数据”和“数据真正从物理文件里消失”是两回事。前面提过InnoDB 删除的行只会在页内留下空洞后续写入会复用这些空间但表空间文件本身不会自动变小。如果你清理数据是为了解决磁盘告警删除之后还必须主动回收空间。6.1 表空间回收OPTIMIZE TABLE 和 ALTER TABLE ... ENGINEInnoDB回收表空间最简单的方式是执行OPTIMIZE TABLE t_log;这条命令在 InnoDB 5.6 版本以后是在线 DDL底层逻辑就是重建整张表把页内的空洞剔除让数据紧凑排布最后让表空间文件缩小。它和你执行ALTER TABLE t_log ENGINEInnoDB基本等价二选一就行。但 OPTIMIZE TABLE 有两个前置条件要特别注意。第一是磁盘空间要足够因为重建表过程中新旧表需要同时占用空间第二是执行期间会有大量 IO 和内存消耗务必挑业务低峰期做。操作完再用SHOW TABLE STATUS或者查information_schema.tables核对 DATA_LENGTH 是否真的降下来。有个小技巧如果表是用分区表管理的OPTIMIZE 可以按分区做比如ALTER TABLE t_log OPTIMIZE PARTITION p202301;一次只整理一个分区压力更可控。普通大表如果整体 OPTIMIZE 时间太长也可以先重建表再 RENAME。6.2 检查主从延迟和积累告警删除之后马上看一下主从延迟尤其是用了批量 DELETE 或重建表方案时。主库删得快不代表从库也同步快历史上见过太多案例主库三小时删完从库追了整整两天期间业务读写分离查到的全是过期的旧数据。检查命令很简单SHOW SLAVE STATUS\G -- 或者 MySQL 8.0.22 之后用 SHOW REPLICA STATUS\G重点看Seconds_Behind_Master或Replica_SQL_Running_State是否归零。如果延迟还在几十秒以上说明从库还在重放 binlog不要急着做下一步操作否则会在集群层面叠加压力。另外批量删除期间主库如果产生了大量行锁等待人为慢查询记录里可能留下一堆历史慢 SQL。我的习惯是清理完以后把慢查询日志阈值临时调低跑一段时间确认没有因为这次删除导致的持续锁等待或大范围全表扫描等系统稳定后再调回去。6.3 从“删除”走向“归档与冷热分离”删除本质上是一种终态操作数据被清掉就再也回不来了。很多业务数据删除其实是“业务不看了但不能丢”这种情况用 DELETE 就不合适更稳妥的方案是归档。你可以先把过期数据导出到归档表或者文件再通过执行策略定期清除也可以直接做成冷热分离热库只放最近三个月数据历史数据导入数仓或廉价存储应用查询时通过接口去冷库取。分区表的 EXCHANGE PARTITION 就是做冷热分离的绝佳手段把旧月份分区转成独立表导出后 DROP主库永远保持轻量。没有分区的表也可以用 pt-archiver 的--dest参数把数据边删边写入归档库。只要设计到“清理”这一步不妨多想一层这个数据是不是真的可以物理消失如果不是预留一套归档路径比你将来被业务追着要数据强得多。7. 我一般怎么选这几种方案处理海量数据删除我的选择顺序基本是这样建表前如果有预期周期性删除直接上分区表方案后期不要折腾如果已经是大表先看删除比例删掉的比例超过七成且能配合短时停写优先用重建表方案比慢慢 DELETE 高效得多如果业务完全不能停写、但又必须原地删那就用游标式分批 DELETE 或者 pt-archiver在可控负载下慢慢磨。把几种方法的适用场景摆在一起看会很直观方案适合场景优点主要风险分批 DELETE百万到千万级数据删除条件能走索引通用、可控、不需要停业务执行时间长大批量下仍有锁竞争重建表RENAME删除比例极大可短时接受写暂停速度快清理彻底空间需求大切换前需要协调业务分区表 DROP/TRUNCATE日志类、按时间周期增长的表秒级清理复制压力小需要建表时规划主键需包含分区键pt-archiver大规模后台持续清理追求稳定自动化、限速、感知从库延迟工具依赖仍需关注索引和字符集最后分享一个我的实操体会批量删除最怕的不是删得慢而是中途失控。任何方案在真正跑之前我都建议先在测试库复制一份真实数据把批量大小、sleep 间隔、总耗时预估都模拟一遍顺便把回滚方案想好。线上操作时也永远别一条命令直接开跑先限制 1 万个主键范围试水确认执行计划、锁等待和从库延迟都在预期内再放开批量跑。清理海量数据这件事慢就是快稳就是快。