PAYATHON 2026

如何用 Stored Procedure 强制删除受 trigger 保护的记录

支付阿杰

结论

SQL Server 没有针对单条 DELETE 的 IGNORE TRIGGER 或 trigger 覆盖语法。只要目标表启用了 INSTEAD OF DELETE trigger,普通 DELETE 都会先进入该 trigger,Stored Procedure 中的 DELETE 也不例外。

如果需要受控地执行物理删除,通常不应在 Stored Procedure 中临时禁用 trigger。可以采用以下方式:

  1. Stored Procedure 设置一个仅供清理操作使用的会话标记。
  2. INSTEAD OF DELETE trigger 检查该标记和执行身份。
  3. 条件满足时,trigger 执行物理删除;否则仍按原规则软删除。
  4. Stored Procedure 在事务中按照外键依赖顺序清理数据,并在成功或失败后清除标记。

trigger 只能决定是否执行物理删除,无法绕过 foreign key。只要还有记录引用该产品,物理删除就会失败。必须先处理这些引用关系,或者明确配置级联删除。

先修正现有 trigger

原 trigger 使用单个变量接收 ProductId:

SELECT @id = ProductId FROM deleted

一条 DELETE 语句可能影响多行,而 deleted 是一个集合。这种写法最终只会处理其中某个不确定的 ProductId。

软删除 trigger 应改为集合操作:

CREATE OR ALTER TRIGGER dbo.TR_Products_InsteadOfDelete
ON dbo.Products
INSTEAD OF DELETE
AS
BEGIN
    SET NOCOUNT ON;

    UPDATE p
    SET p.Deleted = 1
    FROM dbo.Products AS p
    INNER JOIN deleted AS d
        ON d.ProductId = p.ProductId;
END;

这样删除一行或多行时,都能正确设置标记。

为管理操作增加受控的物理删除分支

可以通过 SESSION_CONTEXT 传递清理标记,同时验证执行上下文:

CREATE OR ALTER TRIGGER dbo.TR_Products_InsteadOfDelete
ON dbo.Products
INSTEAD OF DELETE
AS
BEGIN
    SET NOCOUNT ON;

    IF TRY_CONVERT(bit, SESSION_CONTEXT(N'AllowProductPurge')) = 1
       AND USER_NAME() = N'dbo'
    BEGIN
        DELETE p
        FROM dbo.Products AS p
        INNER JOIN deleted AS d
            ON d.ProductId = p.ProductId;

        RETURN;
    END;

    UPDATE p
    SET p.Deleted = 1
    FROM dbo.Products AS p
    INNER JOIN deleted AS d
        ON d.ProductId = p.ProductId;
END;

在 INSTEAD OF trigger 内再次对同一基表执行 DELETE 时,SQL Server 会执行实际删除,不会再次调用同一个 INSTEAD OF trigger。不过,foreign key、其他约束和适用的 AFTER DELETE trigger 仍会生效。

同时检查 USER_NAME() = N'dbo',可以防止普通连接仅通过设置 SESSION_CONTEXT 绕过软删除规则。这个检查需要与 Stored Procedure 的执行上下文配合。

创建专用清理 Stored Procedure

下面是一个基本模式,其中的关联表名称需要按实际数据库结构调整:

CREATE OR ALTER PROCEDURE dbo.PurgeProduct
    @ProductId int
WITH EXECUTE AS OWNER
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    BEGIN TRY
        BEGIN TRANSACTION;

        EXEC sys.sp_set_session_context
            @key = N'AllowProductPurge',
            @value = 1;

        /*
          必须根据实际 foreign key 依赖,从子表向父表删除。
          以下表名仅用于说明。
        */

        DELETE FROM dbo.PaymentDetails
        WHERE OrderId IN
        (
            SELECT ol.OrderId
            FROM dbo.OrderLines AS ol
            WHERE ol.ProductId = @ProductId
        );

        DELETE FROM dbo.OrderLines
        WHERE ProductId = @ProductId;

        /*
          这条语句会进入 INSTEAD OF DELETE trigger。
          trigger 发现清理标记和 dbo 执行上下文后执行物理删除。
        */
        DELETE FROM dbo.Products
        WHERE ProductId = @ProductId;

        EXEC sys.sp_set_session_context
            @key = N'AllowProductPurge',
            @value = NULL;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        EXEC sys.sp_set_session_context
            @key = N'AllowProductPurge',
            @value = NULL;

        IF XACT_STATE() <> 0
            ROLLBACK TRANSACTION;

        THROW;
    END CATCH;
END;

WITH EXECUTE AS OWNER 会让过程在其所有者的数据库执行上下文中运行。通常情况下,过程及其所在 schema 的所有者是 dbo。如果数据库使用了不同的所有权设置,就不能假定 USER_NAME() 一定返回 dbo。此时需要调整身份检查,也可以使用证书签名进行更严格的权限控制。

执行权限只应授予专门的管理角色:

CREATE ROLE ProductPurgeAdministrators;

GRANT EXECUTE ON dbo.PurgeProduct
TO ProductPurgeAdministrators;

普通应用账号不应具有直接删除产品、模拟高权限用户,以及修改该过程或 trigger 的权限。

不建议临时禁用 trigger

从技术上说,可以在过程中这样处理:

DISABLE TRIGGER dbo.TR_Products_InsteadOfDelete ON dbo.Products;

DELETE FROM dbo.Products
WHERE ProductId = @ProductId;

ENABLE TRIGGER dbo.TR_Products_InsteadOfDelete ON dbo.Products;

但这种方式有较大风险:

  • DISABLE TRIGGER 作用于整张表,而不是当前会话。
  • trigger 被禁用时,其他连接也可能物理删除记录。
  • 这种方式需要更高的权限。
  • 如果异常处理不完整,trigger 可能长时间处于禁用状态。
  • 禁用 trigger 仍然无法绕过 foreign key。

这种做法更适合在停机维护窗口中由运维人员操作,不适合日常在线 Stored Procedure。

数据模型方面的注意事项

物理清除产品及其订单、付款和财务记录,不只是表空间问题,还关系到审计、对账、退款、税务和数据保留要求。删除前至少要确认:

  • 哪些订单和付款记录依法或按照业务要求必须保留。
  • 应该删除交易记录,还是只匿名化客户信息。
  • 订单明细是否保存了产品名称、成交价格、税率等历史快照。
  • 是否有审计日志记录清理人、清理时间、原因和受影响的行数。
  • 是否要先将历史数据迁移到归档表或归档数据库。

软删除造成表数据增长,并不表示必须删除历史交易。常见的处理方式是为 Deleted、状态列和业务查询建立合适的索引,再定期归档已经达到保留期限的数据。确认依赖关系和数据保留政策后,才能调用物理清理过程。

备注:内容仅供参考。