必须同时关闭数据库级recursive_triggers和实例级nested triggers开关才能彻底禁用触发器递归;仅关前者无效,因后者默认为1且控制底层递归闸门。

SQL Server 触发器递归爆栈的两个开关必须一起关
只关 RECURSIVE_TRIGGERS 不够,nested triggers 服务器级开关才是底层闸门。默认值为 1,意味着只要触发器内执行同表 DML,哪怕数据库设了 RECURSIVE_TRIGGERS = OFF,仍可能间接递归。
检查当前状态:
-
SELECT DATABASEPROPERTYEX(DB_NAME(), 'IsRecursiveTriggersEnabled')—— 返回1表示数据库级已开 -
EXEC sp_configure 'nested triggers'—— 第二列值为1即启用
真正禁用需两步同时执行:
ALTER DATABASE [your_db] SET RECURSIVE_TRIGGERS OFFEXEC sp_configure 'nested triggers', 0; RECONFIGURE
注意:RECURSIVE_TRIGGERS 是数据库级,切换 DB 后要重新确认;nested triggers 是实例级,影响所有数据库。
触发器里更新同表是爆栈最常见路径
AFTER 触发器中写 UPDATE same_table SET ... WHERE id IN (SELECT id FROM inserted),若该表还存在 AFTER UPDATE 触发器,就构成隐式递归链。SQL Server 不做深度检测,只判断“是否触发本表另一触发器”,满足即压栈,约 100 层后直接 StackOverflowError。
典型错误现象不是立刻报错,而是:
- 执行看似正常语句时连接突然中断
- SSMS 中卡死、无响应
- 日志出现
Event loop exception和StackOverflowError(易被误判为 DBeaver 客户端崩溃)
验证方式:在触发器内加 SELECT TRIGGER_NESTLEVEL(),插入后查返回值 ≥ 2 就说明已进入递归。
比关开关更可靠的是在触发器开头加防护
开关全关是兜底,但业务逻辑若真依赖触发器链(极少见),硬关会破坏功能。更稳妥的做法是在触发器第一行主动退出:
-
IF TRIGGER_NESTLEVEL() > 1 RETURN—— 简单有效,跳过深层调用 - 避免用
@@SPID+sys.dm_exec_sessions判断程序名,性能差且不可靠 - 绝不把更新逻辑留在触发器里:优先改用
INSTEAD OF触发器接管,或把 DML 移到应用层
若必须保留触发器内更新,至少确保该更新不命中任何其他触发器——例如加 WHERE 条件排除被监控字段,或用临时表中转再由外部脚本分批处理。
DBeaver 切换连接时的“伪堆栈溢出”容易混淆真实问题
在 DBeaver 中从 sql_user_1 切到 sql_user_2 后执行含触发器的脚本,报 StackOverflowError + Can't get column 'is_hidden',大概率不是 SQL Server 爆栈,而是 DBeaver 23.3.5 之前版本解析触发器元数据时陷入递归解析循环。
这类客户端栈溢出特征明显:
- 仅在连接切换后复现
- 错误发生在元数据加载阶段,而非 SQL 执行阶段
- 升级 DBeaver 至
23.3.5+即可修复(官方已确认)
真正服务端触发器递归,不会因客户端切换而变化;排查前务必先确认错误源头——别在 SQL Server 上白调开关。











