ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

MySQL海量数据批量删除:5种方案与实战踩坑指南

MySQL海量数据批量删除:5种方案与实战踩坑指南 咱们搞MySQL的十有八九都撞上过这么个需求线上有个几千万行的大表要删掉其中大部分数据。可能是一批历史日志、一批过期订单或者按某个业务维度清理无效数据。我见过太多新手上来就是一条DELETE FROM big_table WHERE create_time 2023-01-01然后执行进去数据库瞬间卡死主库连接打满从库延迟一路飙升最后闹到要重启实例。这篇文章就专门聊这个——MySQL里批量删除海量数据到底有哪些靠谱路子各种方案适合什么场景实际操作中又会踩到什么坑。先说清楚一个核心认知删除海量数据的难点从来不在“删除”本身而在删除动作引发的一连串连锁反应。你要删1000万行MySQL要逐行标记删除、记录binlog、维护二级索引、写入undo log供事务回滚和MVCC使用。这些操作都会落在磁盘和内存上加上行锁、间隙锁互相争抢最终表现为数据库性能断崖式下跌。理解了这一层你就能理解为什么所有人都说“大表不能直接DELETE”了。这篇文章覆盖的分批删除、按主键范围分段删除、分区表清空、建新表切换、pt-archiver工具归档这几类主流方案都是我实际用过的。我会把每个方案的原理讲明白给出可以直接抄的SQL和脚本再把我踩过的坑一并列出来。适合正在为清理大表发愁的DBA、后端开发以及所有被慢查询日志吓到过的同学。1. 先搞明白为什么直接DELETE会“删崩”数据库1.1 你删的不只是数据还有一堆“隐形成本”很多人以为DELETE就是把磁盘上那几行数据划掉但实际上在InnoDB存储引擎里一次DELETE消耗的资源远超你想象。咱们一条条拆。第一行锁和间隙锁的覆盖范围。一条不带精确条件的DELETE比如DELETE FROM orders WHERE status expired在RR隔离级别下InnoDB会扫描所有匹配到的记录并对它们加锁。如果这个条件没走索引那就是全表扫描加锁等于把整张表锁了个遍。就算走了索引大量行同时加锁锁冲突的概率也急剧上升直接影响同一张表所有其他读写请求。第二undo log膨胀带来的连锁反应。InnoDB的事务隔离靠MVCCMVCC靠undo log保存历史版本。你删除多少行就要往undo log里写多少条反向操作记录。1000万行的DELETEundo log随便就是好几GB。这还不是最要命的更要命的是这些undo log在事务结束前不能清理它们对应的旧版本数据还会被其他事务读到于是在repeatable read隔离级别下长事务大删除组合会让undo表空间暴涨到撑爆磁盘。第三binlog的传输放大。MySQL主从同步依赖binlog默认情况下DELETE产生的binlog是statement格式一条SQL在主库刷一遍、在从库也要刷一遍。但如果你开了row格式很多生产环境为了数据安全都会开那么1000万行就会生成1000万条binlog记录事件主从同步的网络开销、从库应用日志的CPU开销立刻拉满。我见过一个案例主库删了500万行用了8分钟从库追了2个小时没追上。1.2 二级索引和碎片删完之后表反而“变大”了InnoDB的二级索引和聚簇索引是分开存储的。删除数据时聚簇索引里的记录被标记删除但二级索引里对应的索引条目不一定能及时回收。大量删除之后索引B树的叶子节点出现大量空位索引扫描效率下降物理文件.ibd不会自动缩小。结果就是你辛辛苦苦删了5000万行SELECT COUNT(*)确实变少了但磁盘占用一点没降查询速度和删除前几乎没区别。后面我讲“重建表”方案时你会看到这才是彻底清理物理空间的唯一出路。1.3 看清楚场景再选方案既然直接删不行那就得找替代方案。但替代方案不是一根筋走到黑得看你具体什么场景。我把实际工作中常见的场景归成三类你可以对号入座场景特征典型例子推荐方案删少量数据几万行以内清理一批用户标记的错误数据直接DELETE加上合适索引删大量数据但希望保留表结构、保留在线服务清理过期日志、过期订单但表还要继续使用分批DELETE、按主键分段删除、pt-archiver删掉大部分数据、或要彻底释放磁盘表内数据基本都没用了只留最近一小部分新建表切换、分区表清空这里头的关键区别在于你要“温和地删”还是“彻底地换”。如果是前一种重点是控制每次删除的量、控制锁和主从延迟如果是后一种重点是让MySQL在极短时间内完成“无痕切换”靠的是DDL级别的原子操作和元数据替换。2. 最通用的方案分批循环DELETE把大事务拆成小事务2.1 核心思路与SQL写法分批DELETE的本质是把一个大事务拆成N个小事务。每删几千行就提交一次锁持有时间短、undo log及时释放、binlog分批传输对在线业务的影响被压缩到最小。这个方案不挑表结构、不挑数据分布适用面最广我建议所有场景都先从这个方案起步。最基础的写法是靠着主键ID范围来分批DELETE FROM big_table WHERE id BETWEEN 1 AND 10000;执行完后继续删下一批:DELETE FROM big_table WHERE id BETWEEN 10001 AND 20000;如果你不想手动算范围可以选择每次取一批要删的主键ID然后用主键IN去删除DELETE FROM big_table WHERE id IN ( SELECT id FROM ( SELECT id FROM big_table WHERE create_time 2023-01-01 LIMIT 5000 ) t );注意这里为什么套了一层子查询MySQL不允许直接在DELETE的WHERE里SELECT同一张表的子查询错误码1093包一层派生表就能绕过这个限制。外层每次触发会扫描到满足条件的5000个ID然后删除。这种方式不依赖ID连续比BETWEEN方式更通用。但它的缺点是子查询本身也要扫描如果筛选条件没走索引性能同样会很差。2.2 批次大小怎么定别拍脑袋看三个指标分批大小的选择直接决定方案的成败。批次太大会退化成大事务太小则删除效率低下循环几万次能把人急死。我从实际操作经验里总结了三个参考指标。一是看主库的锁等待和活跃会话数。单批次DELETE执行期间SHOW ENGINE INNODB STATUS里面的History list length不能持续暴涨活跃会话也不能长时间趴满。如果单批次5000行执行时间超过2秒就要减小批次。二是看从库的延迟情况。设置定时任务循环删除的场景每次循环之间SLEEP几秒钟。用SHOW SLAVE STATUS观察Seconds_Behind_Master如果延迟在增长说明删除速度超过从库应用binlog的速度必须加大间隔或减小批次。三是看undo表空间增长速度。一次性删太多可用SHOW GLOBAL STATUS LIKE Innodb_history_list_length观察这个值代表未清理的undo日志量如果它持续高位不下降说明系统里有长事务或者删除太快来不清理这种时候加大SLEEP时间。以一个线上案例说某张2亿行的用户行为日志表按天清理60天前的数据每次删8000行删除耗时约1.2秒循环之间SLEEP 20秒。整体算下来每秒约删除400行左右主库负载稳定在20%以内从库延迟控制在5秒内。如果按每次50000行去删单次执行时间就飙到8秒从库延迟直接涨到两分钟以上这就不行了。2.3 循环脚本的完整写法生产环境里我更推荐用存储过程来做一来避免应用层频繁发起连接二来可以在数据库端精确控制循环逻辑。下面这个存储过程是我经常用的模板你按自己的表结构调整下就能用DELIMITER $$ CREATE PROCEDURE batch_delete_big_data(IN p_batch_size INT, IN p_sleep_seconds INT) BEGIN DECLARE v_rows INT DEFAULT 1; WHILE v_rows 0 DO DELETE FROM big_table WHERE create_time DATE_SUB(NOW(), INTERVAL 90 DAY) ORDER BY id LIMIT p_batch_size; SET v_rows ROW_COUNT(); COMMIT; IF v_rows 0 THEN DO SLEEP(p_sleep_seconds); END IF; END WHILE; END$$ DELIMITER ;这个存储过程有两个核心设计值得说说。第一用了ORDER BY id LIMIT ?的形式保证每次删除的都是最老的一批数据而且走主键有序扫描效率高。第二每删完一批就COMMIT一次让事务及时结束undo log和行锁都在每个批次结束后立即释放。还有一个细节很多人在循环里纠结要不要commitMySQL默认是自动提交的但存储过程的事务控制最好还是显式写清楚防止将来修改隔离级别时行为变化。调用方式很简单CALL batch_delete_big_data(5000, 5);这里我解释一下参数怎么配p_batch_size从5000起步往上调p_sleep_seconds从5起步往上加。每次加参数跑一小段观察主库负载和从库延迟找到当前机器配置下的最优组合。内存大、磁盘快SSD、从库延迟容错高的环境可以把批次加大到10000。我自己的经验是宁肯稍保守一点别追求一次删太多导致业务抖动。2.4 分批DELETE的两个主要局限这个方案虽然通用但有两个短板你得知道。第一它只解决了“删除动作”对数据库的压力没解决“物理文件不释放”的问题。分批删完数据后表空间依然是满的.ibd文件大小不会缩。如果你清理数据的目的是为了腾出磁盘空间那分批DELETE并不能直接满足你需要配合后面说的重建表。第二如果业务高峰期删数据即使每批只有几千行依然可能影响性能。这个方案更适合业务低峰期执行比如凌晨1点到5点尽量不要在白天大流量时段跑。3. 主键有序场景下的高效方案按主键范围分段删除3.1 原理顺序扫描远比随机扫描划算如果说分批DELETE是“小步快跑”那按主键范围分段删除就是“化整为零分区块推平”。它的思路是先把要删除的数据主键范围划分成若干连续的区间然后逐个区间删除。比如你要删掉id从1到1亿之间的部分数据就可以分成1000个区间每个区间10万行按顺序依次删。为什么这样更快因为InnoDB聚簇索引本身是按主键组织的B树以主键范围删除时扫描和删除都是顺序的充分利用了磁盘预读和内存缓存。相反如果按create_time等条件去随机找数据每一次定位都要走二级索引再回表频繁的随机IO会大大拉低速度。这个方案比较适合那些主键和业务删除条件高度相关的场景。比如说订单表主键就是订单自增ID而你要删的都是半年前的订单那这些订单的ID一定集中在一个相对靠前的区间按ID范围分段就非常合适。如果主键是UUID或者完全无序的字符串这个方案优势就不明显了。3.2 怎么找分段边界MAX/MIN查询代替COUNT(*)很多人在分段前习惯先执行一次SELECT COUNT(*) FROM big_table WHERE create_time xxx想确认总共有多少数据要删。我劝你不要这么做。大表上COUNT(*)是全表扫描或大范围索引扫描光这一下就能把数据库拖垮。正确做法是直接用MIN(id)和MAX(id)定位边界然后按固定步长推进SELECT MIN(id), MAX(id) FROM big_table WHERE create_time 2023-01-01;比如查出来要删的数据id分布在 1000000 到 58000000 之间那你就可以从1000000开始每100000为一个区间依次删除DELETE FROM big_table WHERE id 1000000 AND id 1100000; DELETE FROM big_table WHERE id 1100000 AND id 1200000;如果你希望全流程自动化也可以写一个存储过程按主键游标推进。但要注意分段删除的每个区间删除量并不均匀——有些区间可能恰好有大量要删的行有些区间则很少。所以单个区间执行时间可能有波动批次间SLEEP仍然不能省。3.3 一个重要优化在删除期间暂停二级索引更新这个技巧我在实践中觉得非常好用但知道的人不多。如果你那张表有多个二级索引每次DELETE都要同时维护这些索引。索引多了删除一行要更新的索引条目也多代价翻好几倍。有一种做法是先拿到要删除数据的id集合存到一张临时表然后删除原表上的二级索引删完数据再重建索引。这里有一个典型Trade-off删除索引期间查原表会变慢因为少了索引但删除数据的整体速度会显著提升。如果你清理的是“要删除大比例历史数据、但近期数据仍然要被业务频繁查询”的表二级索引的维护成本占了删除总开销的大头这个优化效果会非常明显。但它也有风险删除索引和重建索引都是大操作重建索引期间表上所有依赖这个索引的查询都会受影响。因此这个技巧我建议只在“删除后要重建索引”的前提下使用并且放在低峰期配合后面的“删除后重建表”一起做。3.4 分段删除的边界与最佳实践这个方案最大的坑是删除的区间里如果混着“不该删”的数据你有多大概率误删所以分段必须建立在“主键范围和业务条件强相关”的基础上。如果两者的相关性弱你得在DELETE的WHERE里同时加上业务条件比如DELETE FROM orders WHERE id BETWEEN 1000000 AND 1100000 AND status expired;这样写会稍微降低删除效率因为多了条件判断但安全性高很多。我自己一般会在条件里保守一点宁可多扫一点数据也不冒误删的险。原因很简单海量删除一旦误删恢复成本比多跑几分钟高得多。4. 表结构允许时的最优解用分区表删数据就是删文件4.1 为什么分区表能“秒删千万行”如果你的表本身做了分区那海量删除的问题几乎被降维化解了。MySQL 8.0支持的分区类型主要是RANGE、LIST、HASH、KEY对时间维度的历史数据清理来说最常用的是RANGE分区。比如订单表按月份做RANGE分区每个分区存一个月的数据。要删除某个月之前的数据直接ALTER TABLE orders DROP PARTITION p202301;注意这个操作是DDL不是DML。InnoDB在DROP PARTITION时做的本质是删除分区对应的物理文件整个操作不产生逐行删除的行为所以速度极快——一个1亿行的分区DROP它可能也就几秒钟。这是所有DELETE方案都比不了的。如果你的业务周期划分不是按月也可以按季度、按年。只要你在分区创建时预留好未来分区的余量也就是提前建好后面几个月的分区清理数据的时候直接drop掉过期分区根本不用写任何DELETE语句。4.2 分区表改造的步骤与注意事项如果你现在这个表还没分区想改成分区表那就要谨慎了。对已有大数据量的表执行分区操作有几种方式最稳妥的是“新建分区表导入数据切换”的方式-- 1. 创建带分区的目标表 CREATE TABLE orders_partitioned ( id BIGINT NOT NULL, order_no VARCHAR(64), create_time DATETIME, ... PRIMARY KEY (id, create_time) ) ENGINEInnoDB PARTITION BY RANGE (YEAR(create_time) * 100 MONTH(create_time)) ( PARTITION p202301 VALUES LESS THAN (202302), PARTITION p202302 VALUES LESS THAN (202303), PARTITION p202303 VALUES LESS THAN (202304), PARTITION p202304 VALUES LESS THAN (202305), PARTITION pMax VALUES LESS THAN MAXVALUE );注意分区表要求分区字段必须包含在主键和唯一索引中。这是MySQL的一条硬性规定“every unique key on the partitioned table must include every column in the partitioning expression”。比如上面我把id和create_time放一起做了联合主键才满足要求。这个规定非常坑人很多表结构改分区失败都是栽在这里。如果你的业务主键是单列id又想按create_time分区那要么改主键为联合主键对现有业务有影响要么放弃分区方案。-- 2. 将历史数据按分区规则插入 INSERT INTO orders_partitioned SELECT * FROM orders WHERE create_time 2023-04-01; -- 3. 业务停写或低峰期切换表名 RENAME TABLE orders TO orders_old, orders_partitioned TO orders;4.3 没有提前规划那就用在线DDL工具做分区如果你的表已经几千万行了尺寸巨大直接在线上执行ALTER语句分区的耗时和风险都让人望而却步。可以考虑使用gh-ost或者pt-online-schema-change这类在线DDL工具来执行分区操作。它们通过触发器或者binlog复制的方式在目标表上重建新结构切换过程几乎不影响读写。但因为涉及复杂的表结构转换我只建议有一定经验的同学操作并且一定要在测试环境完整演练一遍再上生产。4.4 分区的维护成本别把“分区”当万能药讲了分区这么多好处我也要泼一盆冷水。分区表不是没有代价的。最明显的一点是某些查询没带分区键时性能可能不升反降。比如你设置了按create_time分区但业务查询经常只按order_no查那MySQL会扫所有分区等于退化成了全表扫描。所以是否上分区要先梳理清楚核心查询的WHERE条件。另外分区本身也是DDL操作每次拆分、合并、删除分区都要小心全局元数据锁MDL的影响。生产环境的表要操作分区依然建议低峰期做最好配合在线DDL工具一起用。5. 腾出磁盘空间的一劳永逸方案新建表 切换 重建5.1 核心思路数据“搬家”而不是逐行“删除”前面说过普通DELETE不管你怎么删物理文件都不会变小。如果你清理海量数据的最终目的是释放磁盘空间或者让表恢复到干净紧凑的状态那正确思路是“把要保留的数据复制到一张新表然后用新表替换旧表”。保留的数据越少新表就越小旧表直接DROP掉即可。这个方案的执行流程是根据业务条件把要保留的数据导到新表。业务停写或低峰期做一次数据补齐和表名切换。DROP掉旧表磁盘空间随之释放。5.2 完整操作步骤以保留近3个月数据为例第一步创建新表。最简单的方式是复制旧表结构CREATE TABLE orders_new LIKE orders;这个语法会复制表结构、索引、自增属性但不会复制数据非常适合用来做换表。第二步把需要保留的数据搬进新表。INSERT INTO orders_new SELECT * FROM orders WHERE create_time DATE_SUB(NOW(), INTERVAL 3 MONTH);这里有一点要注意如果保留数据量也很大比如上亿行INSERT不进去那么快而且会产生大量binlog。可以考虑在业务低峰期执行或者用INSERT INTO ... SELECT ... WHERE ...加上分批处理逻辑和前文的分批删除逻辑相通。无论如何绝对不要在业务高峰期执行这个INSERT SELECT它会对源表加锁可能导致线上写入被阻塞。第三步切换表名。这里最安全的是用两个原子性RENAMERENAME TABLE orders TO orders_old, orders_new TO orders;MySQL的RENAME TABLE是原子操作也就是说这一步要么全部成功要么全部失败中间不会有“表不存在”的间隙。这也是为什么推荐用两条RENAME而不是先DROP旧表再RENAME新表。如果先DROP再RENAME中间那几秒你线上所有访问orders的语句都会报“table doesnt exist”。切换完成后应用立刻开始访问新表。新表里只有近3个月数据所有查询都会比原来快不少。第四步验证后DROP旧表。-- 验证新表数据量、索引、自增值都正常 SELECT COUNT(*) FROM orders; SHOW INDEX FROM orders; -- 确认无误后删除旧表释放磁盘空间 DROP TABLE orders_old;DROP表的瞬间会释放磁盘文件这个过程中I/O会有些波动但对业务基本无感。需要注意的是DROP一张几十GB的大表本身也可能产生瞬时I/O压力。如果表特别大比如超过100GB可以在低峰期执行或者考虑用硬链接方式分批释放文件这里不展开知道有这回事就行。5.3 这个方案有一个必须处理的脏数据问题新建表切换方案最大的隐患在于数据一致性。从“把数据复制到新表”到“切换表名”之间旧表可能又有新的写入或修改如果你没有完全停写的话。这些增量数据不会自动出现在新表里。要规避这个问题要么在完全停写窗口内完成第二步和第三步适合可以接受短时间停服的场景要么利用binlog或应用层双写来补齐增量。对于大部分互联网业务来说最现实的是选一个业务量最低的时间窗口比如凌晨2点申请5到10分钟的只读维护窗口一口气完成复制和切换。这个方案在架构上是最干净的也是我推荐你在数据量特别大、清理比例又特别高时优先考虑的路径。5.4 什么时候“换表”优于“删除”这里给个简单的经验总结。假设表总行数X要保留行数Y。如果Y/X的比例低于30%换表方案的综合效率显著高于DELETE方案。因为这种情况下你要拷贝的数据少新表很小切换后所有查询受益磁盘也彻底释放。如果Y/X超过50%新建表拷贝的数据量太大反而不如分批DELETE碎片整理划算。记住这个比例分界线你做技术选型时就不用再纠结了。6. 生产环境更优雅的归档工具聊聊pt-archiver6.1 pt-archiver到底解决了什么问题如果你不想自己写存储过程、又要批量删除归档数据Percona Toolkit里的pt-archiver就是为这个场景量身定做的。它是业界最常用的MySQL归档和清理工具核心优势有三点一是自动分批每次操作一行或一批控制锁粒度二是可以同时把删除的数据插入到归档表三是不要求你手写循环逻辑参数化配置就行。它适合的场景包括定时清理归档历史数据、大表冷热数据分离、删除时保留一份备份到归档库比如归档到另一张表或者另一个实例。我自己在生产上用过它清理过一张几千万行的流水表工具平滑度非常高没有出现锁等待或从库延迟异常。6.2 一条命令教会你用pt-archiver基本用法如下需求是把orders表里2023年之前的数据删除同时把删除的数据备份到orders_archive表pt-archiver \ --source h127.0.0.1,P3306,uarchive_user,pxxx,Dtest,torders \ --dest h127.0.0.1,P3306,uarchive_user,pxxx,Dtest,torders_archive \ --where create_time 2023-01-01 \ --limit 1000 \ --txn-size 1000 \ --sleep 0.5 \ --bulk-delete \ --bulk-insert \ --statistics参数含义拆解一下--source和--dest源表和目标表。如果只做删除不需要归档可以省略--dest。--where删除条件这是核心筛选条件注意这里必须走索引否则工具会提示全表扫描风险。--limit 1000每批取多少行。--txn-size 1000多少行提交一次事务。这两个参数配合控制锁粒度。--sleep 0.5每批之间的休息时间单位秒用来控制删除速率这是保护从库不延迟的重要参数。--bulk-delete和--bulk-insert使用批量删除、批量插入的方式比逐行操作高效得多。--statistics输出执行统计方便事后分析。在实际执行前官方工具支持--dry-run参数只做语法检查而不真正执行可以拿它先验证命令是否正确pt-archiver --source ... --where ... --dry-run6.3 使用pt-archiver的3条实践经验第一连接账号权限最小化就好只需要源表SELECT、DELETE目标表INSERT的权限别一上来就给root。这不仅是安全习惯也能防止操作失误时影响面扩大。第二如果目标归档表和源表结构完全一样--dest指定的表可以提前建好否则工具会自动尝试创建但字段映射可能出错。我建议手动先建好归档表不要依赖工具自动建表。第三必须确认WHERE条件能走索引。pt-archiver执行时会做 explain 检查如果发现全表扫描它有--no-check-charset之类的参数跳过一些检查但全表扫描删除依然会拖垮库。执行前用EXPLAIN看一眼执行计划最稳妥。7. 删除之后的善后工作表空间整理与索引维护7.1 为什么删完数据表文件还是那么大回到前面提到的那个历史遗留问题DELETE只做逻辑删除标记记录为“已删除”物理空间不会立刻返还给操作系统。InnoDB表空间内部虽然可以复用这些空间给后续INSERT但是文件大小不缩。因此如果你执行完大批量DELETE之后用ls -lh看.ibd文件会发现文件大小一点没变。如果你的目标是压缩物理文件目前通用的做法是重建表。在MySQL 5.7之后的版本里执行ALTER TABLE big_table ENGINEInnoDB;可以重建主表数据。这个操作在MySQL 5.6以后是Online DDL允许在重建期间继续读写但它仍然会消耗额外的磁盘空间因为要生成临时表文件耗时也比较长依旧建议在低峰期操作。MySQL 8.0还有一种新姿势用ALTER TABLE ... ALGORITHMINPLACE配合在线操作但也要看版本和表结构具体情况。最稳妥的仍是按前面第5节说的“建新表切换”方案彻底搬家一次物理和逻辑上都干干净净。7.2 清理碎片和重建二级索引另外一个容易被忽略的问题是索引碎片。大量删除后二级索引叶子节点会有很多空位索引空间利用率下降查询性能受影响。除了重建表你还可以对某个二级索引单独做整理ALTER TABLE big_table DROP INDEX idx_create_time, ADD INDEX idx_create_time (create_time);这个操作的本质是删索引再重新建索引索引重建期间相关查询会变慢。如果有多个索引需要重建建议一个一个来别批量操作避免表上长时间没有可用索引。7.3 统计信息更新ANALYZE TABLE不能省大量删除之后MySQL的优化器基于采样得到的统计信息可能已经严重过时于是执行计划就可能出错本该走索引的查询走了全表扫描本该用小索引的查询选了个大索引。高负载下你还会看到information_schema.tables里的数据行数完全对不上那里面的值是估算的不是实时的。这时候就需要主动更新统计信息ANALYZE TABLE big_table;这个操作在MySQL 8.0里执行很快因为它只是重新采样计算统计信息不需要重建数据。在批量删除后的当天夜里或第二天的低峰期执行一次能有效避免优化器误判。8. 常见问题排查与踩坑实录8.1 问题一删着删着从库延迟越来越大这应该是最常遇到的现象。分批DELETE本身没问题但如果你批次太大、SLEEP太短从库应用binlog的速度就跟不上主库产生binlog的速度。排查流程先执行SHOW SLAVE STATUS\G看Seconds_Behind_Master确认延迟数值和趋势再看主库上是不是有长事务在跑information_schema.innodb_trx里如果有TIME很大的事务优先确认是不是自己的删除脚本没提交。最终解法就是把批次调小、SLEEP调大或者在脚本里增加“延迟超过阈值自动暂停”的逻辑这个逻辑不算复杂但非常实用。8.2 问题二DELETE走了索引还是很慢很多情况下你确实给WHERE条件加了索引但执行计划依然选择了全表扫描。原因多半是数据分布让优化器认为走索引不如全表扫描划算。比如要删除的数据占了表中大部分行优化器估算下来全表扫更快。排查方法是执行EXPLAIN DELETE ...看type列和key列如果发现是ALL或者key为NULL那么要么用FORCE INDEX强制走索引要么换成“按主键分段删除”的写法绕开优化器的错误判断。8.3 问题三删除过程中出现锁等待超时错误信息一般是Lock wait timeout exceeded; try restarting transaction。这说明你的DELETE在等一把别人持有的锁。排查方式用SHOW ENGINE INNODB STATUS查看当前锁等待也可以直接查sys.innodb_lock_waits视图看谁堵了谁。通常的原因有两个一是业务本身正在高频更新你要删除的数据范围这种场景要错峰删除二是你的删除批次太大单批持有大量行锁导致后续删除互相等待。解法减小批次、增加SLEEP、尽量在低峰期执行。8.4 问题四删除期间undo表空间暴涨删除动作写undo是必然的但暴涨到撑爆磁盘就有问题了。先看SHOW GLOBAL STATUS LIKE innodb_history_list_length如果这个值一直增加不降说明有老事务一直没结束导致undo日志无法purge。排查information_schema.innodb_trx里是否有长时间未提交的事务尤其注意是不是有APP服务开启了事务但没提交。一个连接空闲但没commit就能让你所有的删除产生的undo都清不掉。遇到这种情况先确认没有长事务再说删除的事否则你删得越快undo涨得越凶。8.5 问题五DROP旧表时磁盘I/O瞬间打满DROP大表时操作系统需要释放对应的大文件。InnoDB删除表空间文件的过程不是瞬间完成的但对磁盘I/O的冲击是真实存在的。如果旧表有100GB以上建议在业务最低峰DROP或者考虑分硬链接释放文件的技巧先把.ibd文件硬链接到另一个路径然后通过TRUNCATE一点点截断文件来释放空间让I/O压力平滑化。这个技巧属于进阶操作普通开发环境不需要用但生产上遇到“不敢DROP大表”的情况可以尝试搜索一下官方文档和相关文章来深入。写在最后的经验聊到这儿批量删除海量数据的几条主流路线基本都过了一遍。我个人的体会是没有万能的方案只有适合当下场景的方案。如果只是日常删几十万行把分批DELETE写好、参数调对就够了如果删的数据占比特别高优先考虑新建表切换如果表结构设计时就想清楚了数据生命周期那么分区表是最省心的长期方案如果公司有Percona Toolkit的运维基础pt-archiver能让你的活变得非常轻松。另外还有一点小建议不管用哪种方案动手前先把表结构和数据分布摸清楚备份必须做好。海量删除不是不能做而是要在可控的节奏里做。你每一次删除前多花十分钟做EXPLAIN、做备份、确认时间窗口线上就能少一次血泪教训。希望这篇文章能帮你把那块压在心口的大石头平稳落地。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表