必须用 THROW 中断删除事务,SQL Server 无 BEFORE DELETE 触发器,仅 INSTEAD OF DELETE 可干预;校验需支持多行、防并发脏读,避免聚合操作与跨表复杂逻辑。

不能靠 RETURN 或 PRINT 拦住删除,必须用 THROW 主动中断事务。
SQL Server 里 BEFORE 删除校验只能用 INSTEAD OF DELETE
SQL Server 没有真正的 BEFORE DELETE 触发器,AFTER DELETE 已经删完了,没法“拦”。唯一能干预删除流程的是 INSTEAD OF DELETE —— 它会替代原 DELETE 操作,由你手动控制是否真删、删哪些、删前查什么。
-
INSTEAD OF触发器中,DELETED表仍可用,但原 DELETE 不会自动执行,得你自己写DELETE FROM ... WHERE ... - 若校验失败,直接
THROW抛异常,事务自动回滚;不要用RETURN,它只是退出触发器,后续语句(比如你手写的DELETE)可能继续执行 - 批量删除时,
DELETED表含所有待删行,校验逻辑必须支持多行(比如用EXISTS+ 子查询,别只取TOP 1)
THROW 是唯一可靠中断方式,RAISERROR + ROLLBACK 易出错
很多人沿用老写法:RAISERROR 后跟 ROLLBACK TRANSACTION。问题在于:如果外层调用者没开启显式事务,ROLLBACK 会报错“没有事务可回滚”,反而掩盖真实业务逻辑错误。
- 用
THROW(SQL Server 2012+)更干净:它自带事务中断语义,不依赖外层事务状态 - 示例:
THROW 50000, '该订单已发货,禁止删除', 1;—— 错误号 ≥50000 表示用户自定义错误,不会被系统忽略 - 避免在
THROW前做耗时操作(如写日志表),否则失败时日志可能残留,且拖慢主路径
关联数据校验必须防并发和脏读
比如“删除用户前检查是否有未完成订单”,这个 SELECT 如果不加锁,可能刚查完订单为空,另一事务立刻插入一条,然后你的 DELETE 就穿过去了。
- 在
INSTEAD OF DELETE中,用SELECT ... WITH (UPDLOCK, HOLDLOCK)锁住关联行(HOLDLOCK等价于SERIALIZABLE),防止其他事务修改或插入 - 不要在触发器里跑
COUNT(*)或SUM(),聚合结果在高并发下不可信,且易锁表;这类规则应前置到应用层或用带版本号的状态字段兜底 - 若校验涉及多个表(如用户 + 订单 + 支付记录),优先用外键级联或应用层分布式锁,触发器只做单表轻量判断(如
status = ''active'')
真正难的不是写触发器,而是厘清校验边界:哪些必须数据库强一致拦住,哪些允许应用层异步补偿。一旦开始在触发器里查三张表再算个总和,你就已经踩进性能与一致性的双重陷阱了。

















