大家好我是数据库小学妹 我踩过的坑你别再踩。去年清历史订单我写了一条 DELETE圈了一千万行。我想着删数据嘛减法跑完就轻了。结果它跑了半个多小时还没完。先来的是慢查询告警。接着主从延迟飙到八分钟磁盘占用也在往上涨。我盯着监控有点懵。数据在减少库怎么越来越重。后来把这条语句拆开看才明白。一条 DELETE 不是一次操作是一千万次操作而且全塞在同一个事务里。删除这件事不是减法InnoDB 删一行干的活比想象中多。它先把行标记成已删再写一条 undo记下这行原来长什么样。回滚要靠它MVCC 也要靠它。同时写 redo保证崩溃后能恢复。开了 ROW 格式的话binlog 里还要记一份前镜像。一行四笔账一千万行就是四千万笔。磁盘上真正少掉的数据要等后台 purge 线程慢慢清。而 purge 得等你这个事务结束。所以删得越多先背上的东西越重。日志先落盘数据页还没清。所以删除过程中磁盘占用常是先涨后降。那天我看到磁盘往上走就是这个原因。undo删得越多背得越重undo 是给回滚用的。事务不结束它就不敢清。一千万行的 DELETE 跑半小时这半小时里 undo 一直长purge 一直追不上。用一条命令就能看这个水位。SHOWENGINEINNODBSTATUS\G-- TRANSACTIONS 段里的 History list length这个数一直涨就说明历史版本在堆积。undo 表空间跟着涨而且只涨不缩。有活跃事务占着它truncate 也做不了。磁盘告警常常不是数据造成的是 undo 撑出来的。undo 涨太多还有个隐性代价。别的查询走 MVCC 时要顺着更长的版本链找可见版本。链越长扫得越慢。你删的是历史数据被拖慢的却是线上查询。更麻烦的是回滚。真跑到一半你受不了KILL 掉会话InnoDB 得拿着 undo 一行一行翻回去。一千万行的回滚可能比删还久。而且这期间它照样吃 IO照样堵着锁。锁提交之前谁都别碰一条 DELETE 是一个事务行锁要持有到提交才释放。删过的那些行别的事务想改只能等。要是条件字段没索引问题更大。InnoDB 锁的是扫描到的索引记录。全表扫一遍再删锁的范围会远超你想象的几行。RR 隔离级别下还有间隙锁锁住的不只是行还有行之间的空隙。我见过一条 DELETE条件字段没索引锁住的是一整段扫描区间。后面的 UPDATE 全在排队等到超时业务开始报错。所以大 DELETE 最难受的地方在锁的时间。它不只是一条语句慢它让一批业务操作一起排队。binlog从库跟不上的那一段ROW 格式下每删一行binlog 里就有一份前镜像。一千万行体积很可观。主库写完还要传给从库从库再一行行回放。主库删得快从库追不上延迟就出来了。那天飙到八分钟业务读从库拿到的是旧数据。binlog 文件也在涨备份体积跟着涨CDC 那边也会被这波流量冲一下。写之前先记一下当前 binlog 大小删完再比一次涨了多少心里就有数。SHOWBINARYLOGS;SHOWREPLICASTATUS\G-- 看 Seconds_Behind_Source 和 Relay_Log_Pos那到底该怎么删动手前先量三件事。要删多少行现在主从延迟多少磁盘还剩多少。行数可以用 EXPLAIN 估别直接跑 COUNT(*)。顺手看一眼 binlog 保留策略别删到一半把日志挤爆。核心思路只有一句把一个大事务拆成很多小事务。注意总工作量没变变的是锁、undo、binlog 分段释放。代价没有消失只是被摊开了。分批不是让删除变快是让它的影响能被控制。维度一条删完按主键分批锁持有到提交才放每批结束就放undo一直涨到提交分段回收binlog一次性写满分段产生中途停止回滚很慢随时能停主从延迟冲得很高可以控速先按主键分批别用 LIMIT 直接切那样每次都要重新扫描定位。-- 每批两千行批间停半秒直到影响行数为 0DELETEFROMt_orderWHEREcreated_at2025-01-01ORDERBYidLIMIT2000;跑完一批SELECT ROW_COUNT()看影响行数。大于零就歇一下再来。停多久看从库延迟抬头就把 sleep 拉长或者把批减小。批大小没有标准答案。我的经验是两千到五千行一批先在从库上试。批太大单批就超过一秒从库照样追不上。更稳的是按主键区间走连排序都省了。DELETEFROMt_orderWHEREid8800000ANDid8802000ANDcreated_at2025-01-01;跑的过程盯四个数。主从延迟、Threads_running、History list length、磁盘余量。哪个不对就停。停是安全的分批本来就为了能停。不想自己写循环pt-archiver 也能做。--limit控批大小--sleep控间隔--commit-each保证每批提交。哪些情况可以一条删完不是所有删除都要分批。几万行、低峰、表不大一条删完更省事。判断的关键是这条语句会造出多大的事务。先看它会摸到多少行再看它要持锁多久。场景做法几万行以内、低峰一条 DELETE 可以百万级、只删一部分按主键分批批间留间隔整表清空用 TRUNCATE别一行行删时间分区表、整段历史先归档再 DROP PARTITION有 CDC、审计、同步链路必须分批盯 binlog 体积整表清空这条特别值得说。很多人习惯DELETE FROM t删得慢undo 和 binlog 还全记一遍。TRUNCATE是重建表快得多日志也少。前提是这张表没有外键引用也没人正拿着它的数据。还有一类最容易被忽略。数据以后可能还要查别急着删。先搬去归档表对完账再删源数据。删错了还能回头这才是稳。避坑清单别一条 DELETE 删百万级。你以为在省事实际是在造一个超长事务。锁、undo、binlog 全跟着它一起膨胀。分批别只加 LIMIT。没有排序或主键条件每批都在重新扫表越删越慢。按主键区间走代价才是稳定的。最后一条是我自己踩的。那次千万行 DELETE 跑不动我直接 KILL想着停下来就没事了。结果回滚比删还久从库延迟反而更高。从那以后我记住删得慢还能停回滚开始就停不了了。现在大删除一律先分批压根不给回滚机会。写在最后删除在业务眼里是减法。在 InnoDB 里它先是一笔账。undo 记着binlog 记着锁压着从库追着。行到后面才真的消失。所以大表删除这道题考的不是 SQL 写得对不对。考的是你会不会把一个巨大事务拆开。拆成能停、能看、能收手的节奏。数据可以分批走。代价不能一笔勾销。你们清理历史数据是一次删完还是分批有没有被回滚坑过评论区聊聊。我是数据库小学妹帮你少走弯路少踩坑咱们下篇见