MySQL的DELETE不是“删完就走”,而是先标记删除再由purge线程异步清理,被删行仍占内存、参与索引遍历并维护所有二级索引,故需用EXPLAIN验证实际扫描行数以控范围。

DELETE 为什么不是“删完就走”
MySQL 的 DELETE 不是直接擦除磁盘数据,而是走“标记 + 清理”两阶段:先在行记录上打删除标记(DB_TRX_ID 变为当前事务 ID,DB_ROLL_PTR 指向 undo log),再由 purge 线程异步回收空间。这意味着:被删的行仍占 buffer pool、仍参与索引遍历、仍要维护所有二级索引条目——尤其当表有 5 个二级索引时,1 行 DELETE 实际要更新 6 处(聚簇索引 + 5 个二级索引)。
EXPLAIN DELETE 显示 type=ALL 就危险了
执行 EXPLAIN DELETE FROM orders WHERE order_date ,如果 <code>type 是 ALL,说明没走索引,正在全表扫描。常见原因包括:
-
order_date列没有索引,或索引被隐式类型转换失效(比如字段是DATETIME,但条件写了'2023-01-01'字符串,MySQL 可能不走索引) - 复合索引顺序错,例如建了
INDEX idx_user_date (user_id, order_date),但 WHERE 只用了order_date,最左前缀不匹配 - 统计信息过期,优化器误判,可手动执行
ANALYZE TABLE orders
大删不加 LIMIT 就等于锁表+日志爆炸
单次删 50 万行,会触发:
- 长事务:undo log 持续增长,可能撑爆
innodb_undo_tablespaces - binlog 暴涨:每行变更都记入 binlog,主从延迟飙升
- 间隙锁(Gap Lock)范围扩大:在
REPEATABLE-READ隔离级别下,WHERE order_date 可能锁住整个索引区间,阻塞其他插入 - buffer pool 脏页刷写压力陡增,IO 打满
正确做法是显式分批:DELETE FROM orders WHERE order_date ,并确保每次都在新事务里执行(<code>AUTOCOMMIT=1 或显式 COMMIT)。
加时间下限能砍掉 80% 扫描行数
即使 order_date 有索引,如果历史数据跨度极大(比如从 2010 年存到 2026 年),WHERE order_date 仍可能扫描数百万行。实际优化中,加一个合理下限效果立竿见影:
DELETE FROM orders WHERE order_date < '2023-01-01' AND order_date >= '2022-01-01' LIMIT 1000;
这个改动让 EXPLAIN 的 rows 从 980 万降到 190 万,执行时间从 18 分钟压到 50 秒。关键点在于:索引扫描范围必须可控,不能只靠“有索引”就高枕无忧。
真正卡住 DELETE 的,往往不是语法,而是你没意识到它背后要维护多少结构、要扫描多大范围、要锁住哪些间隙。越大的表,越要拿 EXPLAIN 看真实扫描行数,而不是凭感觉写 WHERE 条件。


















