直接 DELETE FROM table WHERE create_time 语句不完整,缺少比较操作符和值,语法错误导致执行失败。

为什么直接 DELETE FROM table WHERE create_time
因为 MySQL(尤其是 InnoDB)在执行大范围 DELETE 时,会为扫描到的每一行加行锁,并可能升级为间隙锁或临键锁;如果扫描行数多、事务长,会导致锁等待堆积,阻塞后续读写。更糟的是,未提交的事务会让 undo log 持续膨胀,甚至触发 Lock wait timeout exceeded 错误。
关键不是“删多少”,而是“一次删多久”——锁持有时间取决于语句执行耗时,而耗时又和单次扫描+删除的行数强相关。
- 避免全表扫描:确保
create_time字段有索引(最好是联合索引中左前缀) - 控制事务粒度:用小批量 + 显式事务代替单条大语句
- 避开高峰期执行:即使分批,也要注意
binlog写入和从库延迟
用 LIMIT + 主键范围分批删除最稳妥
推荐基于自增主键(如 id)分段,比纯时间条件更稳定:时间字段可能重复、缺失、或索引选择性差,容易导致每次删的行数波动大,甚至跳过数据。
实操步骤:
- 先查出待删数据的最小和最大
id:SELECT MIN(id), MAX(id) FROM orders WHERE create_time < '2022-01-01';
- 按固定步长(如 5000)循环删除:
DELETE FROM orders WHERE id BETWEEN 10000 AND 14999 AND create_time < '2022-01-01' LIMIT 5000;
- 每次执行后加
SLEEP(0.1)(应用层控制),避免对 I/O 和复制造成脉冲压力
注意:AND create_time 不能省——防止主键范围外混入新数据(比如归档期间有脏写)。
用 ROW_COUNT() 判断是否删完,别依赖 COUNT(*)
执行完一批 DELETE ... LIMIT N 后,用 SELECT ROW_COUNT() 获取实际影响行数。它返回本次语句真正删除的记录数,比先 SELECT COUNT(*) 再删更安全:前者可能因并发插入/删除导致计数失真,且多一次全表扫描。
- 如果
ROW_COUNT()返回 0,说明当前批次无匹配数据,可终止循环 - 如果始终返回满额(如一直是 5000),说明还有数据,继续下一批
- 不要用
SELECT COUNT(*)做循环条件——对亿级表,这一步本身就会卡住
mysqldump 的 --where 不适合删数据,但可辅助验证
mysqldump --where="create_time 只是导出快照,不删数据,但它能快速验证你的过滤条件是否命中预期记录,尤其当你怀疑索引没生效或时间字段类型是 <code>datetime vs timestamp 导致隐式转换时。
真正删之前,建议先跑一遍:
EXPLAIN DELETE FROM orders WHERE create_time < '2022-01-01' LIMIT 1;看
key 列是否用了目标索引,rows 是否明显小于总行数。
最容易被忽略的一点:分批脚本里没设 autocommit=1 或没显式 COMMIT,导致每个 DELETE 都在同一个长事务里——锁不会释放,undo log 累积,最终 OOM 或被 kill。务必确认每批都是独立短事务。

















