必须用 BEFORE DELETE 触发器实现级联删除,因其可在父记录删除前基于 OLD.id 安全定位并清理子表数据,保证事务原子性;AFTER DELETE 因父记录已删、不参与回滚且易触发 ERROR 1442,无法可靠实现级联。

必须用 BEFORE DELETE,不能用 AFTER
触发器要真正“级联删除”,只能选 BEFORE DELETE。因为你要靠 OLD.id 去定位子表里该删的行;AFTER 一执行,主表那行已经物理消失了,OLD.id 还能用,但子表 WHERE 条件若依赖主表其他字段(比如 OLD.status),就拿不到值了——更关键的是,MySQL 在 AFTER 里禁止修改外键关联的表,大概率报 ERROR 1442。
常见错误现象:Can't update table 'orders' in stored function/trigger because it is already used by statement...,哪怕你删的是 users,只要 orders.user_id 是外键,MySQL 就可能直接拦截。
-
BEFORE中删子表,整个操作在同一个事务里,原子性强:主表删失败,子表也不会被删 -
AFTER即使侥幸跑通,外层事务回滚时,子表数据已丢,无法恢复 -
NEW.id在DELETE触发器中不存在,写它等于写NULL,条件失效导致全表误删
WHERE 条件必须精确匹配外键字段
清理逻辑的核心是“只删该删的”,不是“删所有看起来像的”。最常见漏洞就是漏写 OLD. 前缀,比如写成 WHERE user_id = id,结果删掉整张 posts 表。
真实场景中容易踩的坑:
- 主表主键是复合键(如
(org_id, role)),就得写WHERE org_id = OLD.org_id AND role = OLD.role - 主表用
UUID当主键,子表字段也得是CHAR(36)或BINARY(16),别让 MySQL 做隐式转换——索引会失效,删不全 - 子表字段类型和主表不一致(比如主表
id是BIGINT UNSIGNED,子表user_id是INT),比较时可能截断或溢出
大表慎用,避免锁表太久
如果子表有百万级 posts 记录,DELETE FROM posts WHERE user_id = OLD.id 会一次性加锁、扫描、删除,阻塞其他查询,甚至拖垮整个库。
这时触发器不该扛重活:
- 优先检查子表有没有
user_id索引:SHOW CREATE TABLE posts看KEY是否覆盖该字段 - 没索引?先加:
ALTER TABLE posts ADD INDEX idx_user_id (user_id) - 真有海量数据(比如日志类子表),触发器里只发轻量通知(如写入
cleanup_queue表),由后台任务分批处理 - 别在触发器里调用存储过程去删——除非那个过程明确不碰任何触发表,否则极易触发
ERROR 1442
比触发器更稳的方案:先看外键
90% 的级联需求,根本不需要手写触发器。ON DELETE CASCADE 是 MySQL 原生支持的,由 InnoDB 引擎在内存里完成,零 SQL 解析开销,事务安全,且自动维护参照完整性。
操作很简单:
- 确认子表已有外键:
SHOW CREATE TABLE orders查是否有FOREIGN KEY定义 - 没有就加:
ALTER TABLE orders ADD CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - 加完之后,
DELETE FROM users WHERE id = 123,orders里对应记录自动消失,不用写一行触发器代码
真正需要触发器的场景极少:比如必须在删主表前写审计日志、主表是 MyISAM(不支持外键)、或者业务逻辑复杂到外键约束无法表达。其余情况,硬上触发器反而增加出错概率和维护成本。


















