在触发器中加入 INSERTED 和 DELETED 表的糟糕性能

db2*_*db2 13 trigger sql-server-2008 sql-server

我在表上有一个 UPDATE 触发器,用于监视从一个特定值更改为任何其他值的特定列。发生这种情况时,它会通过单个 UPDATE 语句更新另一个表中的一些相关数据。

触发器所做的第一件事是检查是否有任何更新的行的该列的值与相关值发生了变化。它只是将 INSERTED 连接到 DELETED 并比较该列中的值。如果没有任何条件,它会提前退出,因此 UPDATE 语句不会运行。

IF NOT EXISTS (
    SELECT TOP 1 i.CUSTNMBR
    FROM INSERTED i
        INNER JOIN DELETED d
            ON i.CUSTNMBR = d.CUSTNMBR
    WHERE d.CUSTCLAS = 'Misc'
        AND i.CUSTCLAS != 'Misc'
)
    RETURN
Run Code Online (Sandbox Code Playgroud)

在这种情况下,CUSTNMBR 是基础表的主键。如果我对此表进行大量更新(例如 5000 多行),即使我没有触及 CUSTCLAS 列,该语句也需要 AGES。我可以在 Profiler 中看到它在这个语句上停滞了几分钟。

执行计划很奇怪。它显示了具有 3,714 次执行和约 1,850 万行输出的插入扫描。它通过 CUSTCLAS 列上的过滤器运行。它将这个(通过嵌套循环)连接到一个删除扫描(也在 CUSTCLAS 上过滤),它只执行一次并且有 5000 个输出行。

我在这里做了什么愚蠢的事情来导致这种情况?请注意,触发器绝对必须正确处理多行更新。

编辑:

我也尝试过这样写(以防 EXISTS 做了一些不愉快的事情),但它仍然一样糟糕。

DECLARE @CUSTNMBR varchar(31)
SELECT TOP 1 @CUSTNMBR = i.CUSTNMBR
FROM INSERTED i
    INNER JOIN DELETED d
        ON i.CUSTNMBR = d.CUSTNMBR
WHERE d.CUSTCLAS = 'Misc'
    AND i.CUSTCLAS != 'Misc'

IF @CUSTNMBR IS NULL
    RETURN
Run Code Online (Sandbox Code Playgroud)

Mar*_*ith 11

您可以使用显式INNER MERGE JOIN或INNER HASH JOIN提示进行评估,但考虑到您可能稍后在触发器中再次使用这些表,您最好将inserted和deleted表的内容插入索引#temp表并完成它。

它们不会自动获得为它们创建的有用索引。


Sol*_*zky 10

我知道这已经得到了回答,但它只是在最近处于活动状态时突然出现,而且对于具有数百万行的表,我也遇到过这种情况。虽然不打折已接受的答案,但我至少可以补充一点,我的经验表明,在进行类似测试(查看一个或多个列是否实际更改了其值)时,触发性能的一个关键因素是列是否已更改被测试实际上是UPDATE声明的一部分。我发现比较inserted和deleted表之间的列实际上不是UPDATE语句一部分会对性能造成巨大的拖累,如果这些字段是UPDATE声明(不管它们的值实际上被改变了)。为什么要进行所有这些工作(即比较 X 行中的 N 个字段的查询)以确定是否有任何更改,如果您可以从逻辑上排除任何这些列被更改的可能性,如果它们不存在,这显然是不可能的在声明的SET条款中UPDATE。

我采用的解决方案是使用UPDATE()函数,该函数仅适用于触发器内部。这个内置函数会告诉您是否在UPDATE语句中指定了列,如果您关心的列不是UPDATE. 这可以与 a 结合使用SELECT来确定这些列(假设它们存在于 中UPDATE)是否有实际更改。我在几个审计触发器的顶部有代码,如下所示:

-- exit on updates that do not update the only 3 columns we ETL
IF (
     EXISTS(SELECT 1 FROM DELETED) -- this is an UPDATE (Trigger is AFTER INSERT, UPDATE)
     AND (
            NOT (UPDATE(Column3) OR UPDATE(Column7)
                 OR UPDATE(Column11)) -- the columns we care about are not being updated
            OR NOT EXISTS(
                        SELECT 1
                        FROM INSERTED ins
                        INNER JOIN DELETED del
                                ON del.KeyField1 = ins.KeyField1
                                AND del.KeyField2 = ins.KeyField2
                        WHERE ins.Column3 <> del.Column3
                                 COLLATE Latin1_General_100_CS_AS -- case-sensitive compare
                        OR    ISNULL(ins.Column7, -99) <> 
                                 ISNULL(del.Column7, -99) -- NULLable INT field
                        OR    ins.[Column11] <> del.[Column11] -- NOT NULL INT field
                      )
          )
    )
BEGIN
    RETURN;
END;
Run Code Online (Sandbox Code Playgroud)

如果出现以下情况,此逻辑将继续执行触发器的其余部分:

  1. 该操作是一个 INSERT
  2. 至少有一个相关字段位于SETan的子句中, UPDATE 并且一行中的这些列中至少有一个已更改

在NOT (UPDATE...) OR NOT EXISTS()可能看起来很奇怪或倒退,但它的目的是为了避免做SELECT的inserted,并deleted表,如果没有相关的列是的一部分UPDATE。

根据您的需要,COLUMNS_UPDATED()函数是另一种确定哪些列是UPDATE语句的一部分的选项。