编程 MySQL 9.7 外键级联开始执行子表触发器:enable_cascade_triggers 的开关、映射与回滚边界

2026-10-17 21:05:31

MySQL 9.7 外键级联开始执行子表触发器:enable_cascade_triggers 的开关、映射与回滚边界

参考:Oracle MySQL 博客:No More Silent Foreign Key Cascades | MySQL 9.7 Reference Manual

以前:级联在 InnoDB 内部完成,子表触发器不执行

MySQL 9.7 之前,ON DELETE CASCADE 这类外键动作由 InnoDB 内部完成,SQL 层不会进入子表的触发器。结果是子表行确实被删了,但挂在子表上的 AFTER DELETE 触发器一行也没跑。审计表、派生数据表、可观测性埋点全部静默漏记,而且不报错、不告警。

这个问题在 MySQL Bug #11472 里记录了近 20 年。

用一组表说明缺口。父表 orders,子表 order_items,级联删除,子表上挂一个审计触发器:

CREATE TABLE orders (
  id      INT PRIMARY KEY,
  status  VARCHAR(16) NOT NULL DEFAULT 'open'
) ENGINE=InnoDB;

CREATE TABLE order_items (
  id       INT PRIMARY KEY,
  order_id INT NOT NULL,
  qty      INT NOT NULL,
  CONSTRAINT fk_items_order FOREIGN KEY (order_id)
    REFERENCES orders(id) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;

CREATE TABLE order_items_audit (
  audit_id BIGINT AUTO_INCREMENT PRIMARY KEY,
  item_id  INT NOT NULL,
  order_id INT NOT NULL,
  action   VARCHAR(8) NOT NULL,
  at       TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

CREATE TRIGGER trg_items_after_delete
AFTER DELETE ON order_items
FOR EACH ROW
INSERT INTO order_items_audit (item_id, order_id, action)
VALUES (OLD.id, OLD.order_id, 'DELETE');

在 9.7 之前的版本执行 DELETE FROM orders WHERE id = 1;,order_items 里该订单的行会被级联删掉,但 order_items_audit 一条记录都不会有。

开关:enable_cascade_triggers,默认 OFF

9.7 新增系统变量 enable_cascade_triggers,用来在 SQL 层外键级联期间执行子表触发器。默认值是 OFF,保持向后兼容——已有触发器如果没考虑过级联路径,开启后行为会变。

SELECT @@enable_cascade_triggers;          -- 默认 0
SET SESSION enable_cascade_triggers = ON;  -- 会话级
SET GLOBAL  enable_cascade_triggers = ON;  -- 全局

哪种级联执行哪种触发器

外键动作子表上执行的触发器
ON DELETE CASCADEDELETE 触发器
ON UPDATE CASCADEUPDATE 触发器
ON DELETE SET NULLUPDATE 触发器
ON UPDATE SET NULLUPDATE 触发器

开关打开后,BEFORE 和 AFTER 触发器都会执行。

注意 SET NULL 走的是 UPDATE 触发器,因为子表行没有被删除,只是被更新成 NULL。审计逻辑要能区分「业务发起的 UPDATE」和「级联 SET NULL」,否则两类事件会混在同一条记录里:

CREATE TRIGGER trg_items_before_update
BEFORE UPDATE ON order_items
FOR EACH ROW
INSERT INTO order_items_audit (item_id, order_id, action)
VALUES (OLD.id, OLD.order_id,
        IF(NEW.order_id IS NULL, 'UNLINK', 'UPDATE'));

验证级联删除是否真的走到了触发器:

SET SESSION enable_cascade_triggers = ON;
DELETE FROM orders WHERE id = 1;
SELECT * FROM order_items_audit ORDER BY audit_id;

再关掉开关做对照,同一张审计表不会新增任何行:

SET SESSION enable_cascade_triggers = OFF;
DELETE FROM orders WHERE id = 2;
SELECT COUNT(*) FROM order_items_audit WHERE order_id = 2;  -- 0

同一语句、同一事务:触发器报错整条回滚

触发器的执行属于同一条 SQL 语句、同一个事务。触发器里报错,整条语句连同已经完成的级联改动一起回滚,父表行也不会被删除。

CREATE TRIGGER trg_items_block
AFTER DELETE ON order_items
FOR EACH ROW
BEGIN
  IF OLD.qty > 100 THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'blocked by cascade trigger';
  END IF;
END;

验证回滚边界:

SET SESSION enable_cascade_triggers = ON;
START TRANSACTION;
DELETE FROM orders WHERE id = 3;
-- ERROR 1644 (45000): blocked by cascade trigger
ROLLBACK;

SELECT COUNT(*) FROM order_items WHERE order_id = 3;  -- 行仍在
SELECT COUNT(*) FROM orders      WHERE id = 3;        -- 父行仍在

同理,触发器在事务里写入审计表的那部分,也随整条语句一起回滚,不会留下「审计写了但数据没删」的半截状态。

回写父表或重入同一级联路径会被报错拦截

触发器如果试图修改父表,或者重新进入同一条级联路径,服务器会检测到并报错。这是防止递归级联和意外副作用:级联删除子表 → 触发器又去动父表 → 再触发一轮级联,路径不收敛。

-- 会报错的写法:子表触发器回写父表
CREATE TRIGGER trg_items_touch_parent
AFTER DELETE ON order_items
FOR EACH ROW
UPDATE orders SET status = 'closed' WHERE id = OLD.order_id;

在 enable_cascade_triggers = ON 的情况下,级联删除走到这个触发器时语句直接失败,不会静默继续。

单条语句的级联表数上限默认 30

一条语句执行过程中,参与级联链的表数有上限,默认 30。超过上限时停止处理并报错,而不是把剩余级联悄悄做完。多级外键链(A → B → C → D…)的库在开启前要确认深度,链路长、且每层都挂触发器时,这条语句会直接失败。

复制行为

  • ROW 格式:副本记录触发器产生的行改动,但不在副本上重新执行触发器。也就是审计表在副本上也会收到对应的行事件,不会因为重放触发器而双写。
  • STATEMENT 格式:使用源库的 enable_cascade_triggers 设置。源库开启、副本未开启(或反之)会导致主从数据不一致,STATEMENT 格式的拓扑必须保证该变量两侧一致。

上生产的顺序

先找出所有挂在子表上的触发器,包括间接挂在多级级联链中间表上的:

SELECT rc.CONSTRAINT_SCHEMA,
       rc.TABLE_NAME            AS child_table,
       rc.REFERENCED_TABLE_NAME AS parent_table,
       rc.DELETE_RULE,
       rc.UPDATE_RULE,
       t.TRIGGER_NAME,
       t.ACTION_TIMING,
       t.EVENT_MANIPULATION
FROM information_schema.REFERENTIAL_CONSTRAINTS rc
JOIN information_schema.TRIGGERS t
  ON t.EVENT_OBJECT_SCHEMA = rc.CONSTRAINT_SCHEMA
 AND t.EVENT_OBJECT_TABLE  = rc.TABLE_NAME
WHERE rc.DELETE_RULE IN ('CASCADE', 'SET NULL')
   OR rc.UPDATE_RULE IN ('CASCADE', 'SET NULL');

然后评估重复审计和重复计数的风险:应用层是否已经手工补记过级联删除、统计表是否在触发器里做过累加、派生表是否依赖触发器维护。开启后同一件事会被记两次或算两次,属于行为变化,不属于 bug。

再按会话级打开跑业务用例,SET SESSION enable_cascade_triggers = ON; 只在当前连接生效,出问题的影响面可控;用例覆盖删除、更新、SET NULL 三类级联之后再考虑 SET GLOBAL。

同版本另一处相关变更:9.7.2 起,JSON Duality View 的 update 先按反向绑定顺序执行 deletes,再执行 inserts 和 updates(Bug #39566804)。

复制全文 生成海报 MySQL 触发器 外键 升级踩坑

推荐文章

程序员茄子在线接单