存储过程里不能直接写COMMIT或ROLLBACK,否则会因@@TRANCOUNT入口与出口不一致触发错误266;正确做法是让调用方控制事务边界,过程内仅用SAVE TRANSACTION实现局部回滚,或启用SET XACT_ABORT ON自动处理运行时错误。

存储过程里不能直接写 COMMIT 或 ROLLBACK,除非你明确控制整个调用链——否则大概率触发错误 266(事务计数不匹配)。
为什么存储过程里写 COMMIT 会报错 266
SQL Server 要求存储过程入口和出口的 @@TRANCOUNT 值必须一致。如果外部已开启事务(@@TRANCOUNT = 1),你在过程里执行 COMMIT,它会让计数变成 0;执行 ROLLBACK 则直接归零——两者都会导致退出时计数失配。
常见错误现象:
- 调用方显式
BEGIN TRANSACTION后执行含COMMIT的存储过程,报错Msg 266, Level 16 - 过程内
ROLLBACK后没补BEGIN TRANSACTION,后续语句继续执行但事务状态异常 - 触发器中误写
COMMIT,直接被 SQL Server 拒绝执行(错误 3609)
SET XACT_ABORT ON 是更可靠的错误兜底方案
比起手动检查 @@ERROR 或写一堆 IF @@ERROR 0 BEGIN ROLLBACK ... END,启用 SET XACT_ABORT ON 更简洁、更健壮。
它让运行时错误(如主键冲突、类型转换失败)自动将事务标记为不可提交(uncommittable),并在批处理结束时强制回滚整个事务。
注意点:
-
SET XACT_ABORT ON不影响编译错误(如语法错、表不存在),这类错误仍会中断执行 - 它只对当前批处理生效,建议放在存储过程开头
- 搭配
TRY...CATCH使用时,CATCH块里必须显式ROLLBACK,因为XACT_ABORT不会自动触发CATCH内的清理逻辑
需要局部回滚?用 SAVE TRANSACTION + ROLLBACK TO
当某一步失败但你想保留前面成功的修改(比如日志写入、状态更新),不能靠外层 ROLLBACK,得用保存点。
示例关键片段:
BEGIN TRAN
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
SAVE TRANSACTION save_before_transfer;
INSERT INTO transfer_log (...) VALUES (...);
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
IF @@ERROR <> 0
ROLLBACK TRANSACTION save_before_transfer; -- 只回退转账,不丢日志
COMMIT;注意:
-
SAVE TRANSACTION不增加@@TRANCOUNT,所以不会破坏计数平衡 - 非
STATIC或INSENSITIVE游标在ROLLBACK TO后会被关闭,容易遗漏 - 保存点名是局部标识符,不能跨过程引用
真正该写 COMMIT/ROLLBACK 的地方在哪
事务边界应该由最外层决定:要么是应用代码(如 C# 的 SqlTransaction),要么是 T-SQL 批处理(显式 BEGIN TRANSACTION → 多个 EXEC proc_x → 最终 COMMIT 或 ROLLBACK)。
存储过程只做两件事:
- 专注业务逻辑,所有 DML 自动纳入外部事务
- 必要时设保存点或抛出错误(
THROW),把“是否提交”决策权留给调用方
最容易被忽略的复杂点:嵌套调用中多个存储过程都设了保存点,而外部批处理又没统一管理 @@TRANCOUNT 变化——这时单看一个过程没问题,合起来却可能因计数漂移导致静默失败。

















