【问题标题】:T-SQL Trigger calling SQLCLR Stored Procedure vs SQLCLR TriggerT-SQL 触发器调用 SQLCLR 存储过程与 SQLCLR 触发器
【发布时间】:2010-07-21 07:33:16
【问题描述】:

从常规 T-SQL 触发器调用 SQLCLR 存储过程而不是直接部署 SQLCLR 触发器有什么好处?

我需要在一个非常大的表中收到特定列更改 (StatusID) 的通知。

目前使用了许多 Windows 服务。每个监控自己的 StatusID,即查询 db 以获取特定的StatusIDSELECT a,b,c FROM t WHERE StatusID = @status

我想尝试将逻辑从服务移动到 SQLCLR 程序集并使用 SQLCLR 触发器调用它们。这是个好主意吗?有更好的想法吗?

【问题讨论】:

    标签: sql-server sql-server-2008 sqlclr database-trigger


    【解决方案1】:

    在我看来,这不需要 SQLCLR。但是,这取决于您对“被通知”的含义。如果可能,我会使用通常的触发器和 SQL 代理作业来执行通知。

    【讨论】:

    • 常规触发器能够调用常规sp,仅此而已,对吧?我需要调用一个 .NET 方法。我对 SQL Broker 进行了一些研究,但这是非常复杂的事情,所以我想到了 SQL CLR
    • 啊,不知道“被通知”意味着其他一些 .NET 代码。不过,SQLCLR 集成带来了一些挑战,正如我刚刚在这里写的:*.com/questions/3063081/…。如果可能,请尝试从其余代码中提取“通知”或调用(如另一个线程中的建议)通过 SQL 代理的 SSIS 包或命令行工具。我对 SQL 代理一无所知 ;-)
    【解决方案2】:

    从常规触发器调用 SQLCLR 存储过程而不是直接部署 SQLCLR 触发器有什么好处吗?

    嗯,有一些不同。这些差异与利弊之间的关系取决于每种情况的具体情况。

    • T-SQL 触发器调用 SQLCLR 对象(存储过程或函数)

      T-SQL 触发器可以通过类似于以下的查询来确定它们附加到哪个表:

      SELECT ss.[name] AS [SchemaName], so.[name] AS [TableName]
      FROM [sys].[objects] so
      INNER JOIN [sys].[schemas] ss ON ss.[schema_id] = so.[schema_id]
      WHERE so.[object_id] = (SELECT so2.[parent_object_id]
                              FROM [sys].[objects] so2
                              WHERE so2.[object_id] = @@PROCID);
      

      从 T-SQL 触发器调用的 SQLCLR 对象将无法直接访问 INSERTEDDELETED 伪表(对于 T-SQL 存储过程甚至 EXEC() 调用也是如此)。这些表可以复制到本地临时表中,然后可以从 SQLCLR 对象(或 T-SQL 存储过程或EXEC() 调用)中读取,但这需要更多时间和 I/O 来创建表,读取从伪表中,并再次将其写入tempdb

    • SQLCLR 触发器

      SQLCLR 触发器可以通过动态 SQL 与 INSERTEDDELETED 伪表交互(这是 T-SQL 触发器无法做到的)。当然,如果 SQLCLR 代码首先需要从伪表中 SELECT,那么您几乎需要使用 SQLCLR 触发器(否则执行愚蠢的 copy-pseudo-tables-to-local-temp-tables 技巧)。

      SQLCLR 触发器无法访问@@PROCID(即使使用Context Connection = true; 创建SqlConnection),因此确实没有简单方法来确定正在修改的表导致触发器被解雇。我强调“简单”,因为可以(虽然我没有测试过)使用 T-SQL 触发器,设置为表上的“第一个”触发器,用表名设置 CONTEXT_INFO,但是你可以不要将CONTEXT_INFO 用于其他任何事情。可能还有另一种方法我几乎已经解决了,但还没有测试过(当我有机会测试时,如果它有效,我会尽量记住在此处更新说明的链接)。

    我需要在非常大的表中收到特定列更改 (StatusID) 的通知。当前使用了许多 Windows 服务。每个监控自己的 StatusID,即向 db 查询特定的 StatusID:SELECT a,b,c FROM t WHERE StatusID = @status。

    这绝对可以更好地处理,并且无需使用 SQLCLR。 SQLCLR 可能很棒,但在必要时应该使用它。在这种情况下,您只需要一个队列表来记录表中更改的行的 PK 列。

    CREATE TABLE dbo.TableChangeQueue
    (
      TableChangeQueueID INT IDENTITY(-2140000000, 1)
                         NOT NULL
                         CONSTRAINT [PK_TableChangeQueue] PRIMARY KEY,
      [a]                datatype_a,
      [b]                datatype_b,
      [c]                datatype_c
    );
    

    然后使用常规 T-SQL 触发器将INSERT 放入该队列。大致如下:

    INSERT INTO dbo.TableChangeQueue ([a], [b], [c])
        SELECT [a], [b], [c]
        FROM INSERTED;
    

    然后每个 Windows 服务都可以从队列表(而不是大表)中读取。处理完记录后,根据它们的TableChangeQueueID 值将它们从队列表中删除。您可以使用DELETE 语句中的OUTPUT 子句同时读取和删除它们,但如果该过程失败,您不希望失去重新尝试处理它们的能力。

    当然,这是一个简单的实现,如果这些行在处理之前多次更新,则允许重复的键条目。如果这是一个问题(即您需要键值是唯一的),那么在 Trigger 中有一些处理方法。请注意,触发器不会更改 Windows 服务正在处理的数据,除非这不会造成问题。

    这里有很多选项,但如果不知道流程是如何工作的,就无法具体说明。但无论如何,主要的一点是将更改的StatusIDs 的处理与 DML 语句分开,因为触发器在内部系统生成的事务中运行。这就是为什么您不想在此触发器中立即处理更改的行,因为它很容易增加对表的阻塞,因为在触发器完成之前事务不会完成;如果触发器出错,则 DML 语句将回滚。

    【讨论】:

      【解决方案3】:

      如果直接从 CLR 调用服务,则必须使用标准 SOAP 绑定,因为 CLR 仅支持 .NET 2.0 样式的 Web 引用。

      使用 SQL Server 服务代理将是一个更强大的解决方案,但正如您所说,它很复杂。使用 SQLCLR 的代价是,如果触发器抛出未处理的错误,更新事务将被回滚。此外,如果您在每次更新时都阻塞服务调用,事务吞吐量肯定会受到影响。

      【讨论】:

      • 确实 SQL Server 支持最高 3.5 的 .NET 版本,但有一些限制。
      • 上次我检查 SQL Server 2008 CLR 不支持 System.ServiceModel,这排除了 WCF。自 R2 发布以来,没有重新讨论过这个问题;希望情况已经改变。