编程 MySQL 9.7 的 enable_cascade_triggers:级联外键会触发子表触发器,复制路径不对称

2026-11-03 21:03:24

MySQL 9.7 的 enable_cascade_triggers:级联外键会触发子表触发器,复制路径不对称

MySQL 9.7.0 发布说明里有一条 WL #17024:SQL 层处理外键时执行的 CASCADE 和 SET NULL 动作,现在可以激活受影响子行上的 BEFORE / AFTER 触发器。开关是新系统变量 enable_cascade_triggers,默认 OFF,按会话设置。

Foreign key CASCADE and SET NULL actions performed by SQL-layer foreign key handling can now activate BEFORE and AFTER triggers on affected child rows. Enable this behavior for a session by setting the new enable_cascade_triggers system variable to ON. The default is OFF. Cascade-induced triggers execute as part of the initiating statement. Trigger errors cause the statement and its cascaded changes to be rolled back. Row-based replication records the changes made by the triggers without reexecuting them on replicas; statement-based replication uses the source's enable_cascade_triggers setting. (WL #17024)

行为:触发器和级联变更属于同一条语句

enable_cascade_triggers 是会话变量,打开后当前会话内的级联动作才会带动子表触发器:

SET SESSION enable_cascade_triggers = ON;
SELECT @@session.enable_cascade_triggers;  -- ON

触发器的类型取决于级联动作:ON DELETE CASCADE 激活子表的 BEFORE DELETE / AFTER DELETE;ON UPDATE CASCADE 激活 BEFORE UPDATE / AFTER UPDATE;SET NULL 会把子表行改成 NULL,因此激活的是子表的 UPDATE 触发器。

CREATE TABLE parent (
  id INT PRIMARY KEY
) ENGINE=InnoDB;

CREATE TABLE child (
  id INT PRIMARY KEY,
  parent_id INT,
  note VARCHAR(64),
  CONSTRAINT fk_child_parent FOREIGN KEY (parent_id)
    REFERENCES parent(id) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;

CREATE TABLE audit_child (
  child_id INT,
  parent_id INT,
  op VARCHAR(32)
) ENGINE=InnoDB;

CREATE TRIGGER trg_child_after_delete
AFTER DELETE ON child
FOR EACH ROW
INSERT INTO audit_child VALUES (OLD.id, OLD.parent_id, 'cascade_delete');

打开变量后执行父表删除,子表每一行的删除都会走到上面的触发器:

SET SESSION enable_cascade_triggers = ON;
DELETE FROM parent WHERE id = 1;
-- child 中 parent_id = 1 的行被级联删除,
-- 每行都触发 trg_child_after_delete,写入 audit_child

把外键换成 SET NULL 时,子表行不会被删除,而是被更新:

ALTER TABLE child
  DROP FOREIGN KEY fk_child_parent,
  ADD CONSTRAINT fk_child_parent FOREIGN KEY (parent_id)
    REFERENCES parent(id) ON DELETE SET NULL ON UPDATE CASCADE;

DELETE FROM parent WHERE id = 1;
-- child.parent_id 被置 NULL,触发 BEFORE UPDATE / AFTER UPDATE

关键点是执行边界:由级联动作引发的触发器属于发起语句的一部分,不是独立语句。这也决定了下面两条语义。

默认 OFF 是为了不改旧行为

在 9.7 之前,级联动作不会激活子表触发器。业务侧如果已经在子表上放了审计、计数、状态流转之类的触发器,升级后行为会直接变:原来只删父行、连带删子行,现在每一行子行都多出一次触发器写入,还可能命中触发器里的约束或 SIGNAL,让原本成功的 DELETE 失败。

默认 OFF 保证升级瞬间语义不变,需要新行为的会话显式打开。它是会话级变量,可以按连接、按任务灰度,而不是一升级全实例生效。

打开之前要评估的点

  • 子表触发器写入放大:一条父表 DELETE 可能级联几十上百行子行,每行都跑一次 BEFORE 和 AFTER。触发器里再写审计表、更新统计表,写入次数按级联行数成倍增长。
  • 审计日志翻倍:应用层审计、触发器审计、binlog 会同时记录同一次级联,日志量和存储按级联规模放大。row-based 下触发器造成的结果也会进 binlog。
  • 锁范围与死锁概率:触发器在级联过程中对其它表加锁,和外层事务已有的锁顺序交叉,死锁概率上升。触发器里再访问父表或其它子表,会拉长级联期间的锁持有时间。
  • 错误传播:触发器里的 SIGNAL、唯一键冲突、死锁都会让发起语句失败,级联变更一并撤销。原本只是“删一行父数据”的操作,现在可能因为子表触发器报错而不可用。

复制:ROW 与 STATEMENT 不对称

同一条级联操作,两种 binlog_format 下副本的行为不同:

  • row-based:binlog 记录触发器造成的结果,副本不重跑触发器。副本上的触发器定义与源不一致,不影响这份数据;但副本本地依赖触发器的副作用(本地审计表、本地计数)不会发生。
  • statement-based:语句被复制,副本执行时按源的 enable_cascade_triggers 设置走,而不是副本自己的变量值。副本上必须有与源一致的触发器定义,否则触发器产生的写入会缺失或报错。

混用 binlog_format 时问题最明显:源是 MIXED,同一类 DELETE 可能这次走 row、下次走 statement,触发器在副本上就时执行时不执行;拓扑里一部分副本从 row 源复制、另一部分从 statement 源复制,两边的审计表会逐渐分叉。切换 binlog_format 或重搭复制前后,也要把这段差异算进去。

核对项:

SHOW VARIABLES LIKE 'binlog_format';
SHOW VARIABLES LIKE 'enable_cascade_triggers';
SHOW TRIGGERS FROM app LIKE 'child';

源和副本的 binlog_format、触发器定义、MySQL 版本都要对齐;低版本副本不识别这个变量,statement-based 路径下更容易出现行为差异。

回滚语义是语句级

触发器报错时,回滚范围是发起语句及其级联变更,不是整个事务。事务在语句失败后仍然存在,由应用决定继续还是回滚。

CREATE TRIGGER trg_child_before_delete
BEFORE DELETE ON child
FOR EACH ROW
BEGIN
  IF OLD.parent_id = 42 THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'cascade blocked';
  END IF;
END;

START TRANSACTION;
DELETE FROM parent WHERE id = 42;
-- 触发器 SIGNAL,父行删除和子行的级联删除一起回滚
SELECT COUNT(*) FROM child WHERE parent_id = 42;  -- 行还在
ROLLBACK;

注意这里撤销的不只是触发器里的写入,还包括已经执行的级联删除本身。应用如果习惯“父表删除失败就重试”,要意识到重试会把整段级联再跑一遍。

9.7.0 同批的其它变更

  • UNIQUE NOT NULL 键在 CREATE TABLE / ALTER TABLE 时被视为 Primary Key 等价,配合 sql_generate_invisible_primary_key 使用。
  • 审计日志支持 audit_log.rotate_on_time。
  • replica_allow_higher_version_source 允许低版本副本连接高版本源。
复制全文 生成海报 MySQL 主从复制 触发器

推荐文章

程序员茄子在线接单