DELETE慢的根源在于FOR EACH ROW触发器被逐行执行,每删一行就运行完整逻辑,若含未走索引查询、自表更新或JSON重复解析,性能急剧下降。

DELETE 本身不慢,慢的是它触发的行级同步逻辑——只要触发器里有一条没走索引的 SELECT、一次自表 UPDATE、或一个重复解析的 NEW.payload,整条 DELETE 就会被拖住。
触发器在 DELETE 中被反复执行 N 次
DELETE FROM orders WHERE status = 'cancelled' 删 5 万行,触发器就执行 5 万次(FOR EACH ROW 是默认行为)。这不是“一次触发”,而是“每删一行就跑一遍完整逻辑”。
- 常见错误现象:
SHOW PROCESSLIST看到大量线程卡在Updating或Sending data;performance_schema.events_statements_history_long里查出同一触发器语句高频出现 - 如果触发器内有
INSERT INTO audit_log VALUES (OLD.id, OLD.user_id, NOW()),且audit_log表没主键或没索引,写入会退化为磁盘追加+全表扫描 - MySQL 不支持
FOR EACH STATEMENT(仅 PostgreSQL / SQL Server 支持),所以无法靠改触发粒度来降频
实操建议:用 ALTER TABLE orders DISABLE TRIGGER trig_after_delete 临时关闭,对比 DELETE 耗时变化,确认是否真由触发器导致瓶颈
触发器里查关联表没走索引
比如DELETE 订单后,触发器要查用户积分:SELECT points FROM users WHERE id = OLD.user_id。看着是单点查询,但若 users.id 是 BIGINT,而 OLD.user_id 是 INT,就会触发隐式类型转换,索引失效。
- 验证方式:把触发器里的
SELECT单独拿出来,用EXPLAIN ANALYZE(PostgreSQL)或EXPLAIN FORMAT=JSON(MySQL)跑一遍,重点看type是否为const/eq_ref,rows是否为 1 -
OLD字段引用别嵌套函数调用:像json_extract(OLD.meta, '$.ref_id')每次都重新解析 JSON,应先赋值:SET @ref_id := json_extract(OLD.meta, '$.ref_id');再用 @ref_id 查询 - 禁止在触发器里写
IN (SELECT ...)或NOT IN—— 一旦子查询返回NULL,整个条件结果为UNKNOWN,优化器可能放弃索引选择
触发器中更新被删表自身或写大日志表
AFTER DELETE 里再对 orders 表做 UPDATE,或往无索引的 log_history 插入 10 个字段,都是高危操作。
- 自表 DML 容易引发死锁或递归(尤其当其他触发器也监听同表时),MySQL 无
pg_trigger_depth()这类防护机制,只能靠业务层加标记字段绕过 - 日志表必须有主键 + 写入时间字段索引,否则
INSERT会随数据增长线性变慢;更稳妥的做法是禁用该触发器,改用CDC或binlog解析异步消费 - 所有触发器逻辑应控制在 3 行有效 SQL 内:比如校验用
CHECK约束替代,计数用生成列替代,审计用应用层埋点替代
真正难处理的不是语法写错,而是默认以为“删一行触发一次”很轻量——实际上它绑定了事务、锁、日志、复制延迟所有链路。千万级表上,哪怕单次触发只多花 2ms,QPS 上 500 就直接打满 IO 和锁资源。

















