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 CASCADE | DELETE 触发器 |
ON UPDATE CASCADE | UPDATE 触发器 |
ON DELETE SET NULL | UPDATE 触发器 |
ON UPDATE SET NULL | UPDATE 触发器 |
开关打开后,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)。