在触发器中将INSERTED和DELETED表联接在一起的糟糕表现


12

我在表上有一个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

在这种情况下,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

您能摆脱“ TOP 1”吗?我认为这会导致一些开销,如果您只是检查是否有单个案例,可能不需要这些开销……
JHFB 2012年

Answers:


10

您可以使用显式INNER MERGE JOININNER HASH JOIN提示进行评估,但考虑到您稍后可能会在触发器中再次使用这些表,则最好将insertedand 的内容插入已deleted索引的#temp表中并进行处理。

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


好的,这极大地加快了速度,但是有可能级联触发器执行。如果我在每个触发器中使用相同的临时表名称(#i,#d),则它们会冲突。有没有比在每个触发器中使用不同的临时表名称更好/更安全的解决方案?
db2 2012年

可以使用表变量(定义了主键CUSTNMBR以创建唯一的聚集索引)进行评估,并使用OPTION (RECOMPILE)提示使其考虑行数,或者仅使用特定的命名约定,例如#i_dbo_YourTable
Martin Smith

我想我会喜欢把它们命名为#trigger_name_i。如果使用表变量,则必须使用显式的CREATE TABLE使代码更加混乱。我们有级联触发器,但没有递归触发器,所以我想我会很安全的……
db2 2012年

为此,我建议使用表变量而不是临时表。表变量仍然可以具有主索引和辅助(唯一)索引,当触发器退出并且表变量的作用域仅限于该触发器执行时,它们将自动清除(它不会与其他名称相同或更高的表变量发生冲突)调用堆栈)。为了节省表定义代码的开销,请为每个表定义一个表类型,然后使用类型名称声明表变量。
克里斯·史密斯

@ChrisSmith您通常也需要OPTION (RECOMPILE)使用基数。
马丁·史密斯,

10

我知道已经回答了这个问题,但是由于最近处于活动状态,它才弹出,对于具有数百万行的表,我也遇到了这个问题。在不忽略接受的答案的同时,我至少可以补充说,我的经验表明,进行类似测试(查看一个或多个列是否实际上已更改其值)时,触发器性能的关键因素是该列是否被测试实际上是UPDATE声明的一部分。我发现,比较实际上不是该语句一部分的表inserteddeleted表之间的列会极大地拖累性能,如果这些字段属于表的一部分,则性能将受到严重影响。UPDATEUPDATE语句(无论其值实际被更改)。如果您可以从逻辑上排除那些列中的任何列被更改的可能性,那么为什么所有这些工作都可以进行(即查询以比较X行中的N个字段)以确定是否有任何更改,如果不存在这些列,则显然不可能在语句的SET子句中UPDATE

我采用的解决方案是使用仅在触发器内部起作用的UPDATE()函数。此内置函数告诉您是否在UPDATE语句中指定了列,如果您关注的列不属于,则可用于退出触发器UPDATE。可以结合使用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;

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

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

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

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


1
他们应该检查的点很重要,UPDATE(CUSTCLAS)如果为假(+1),则跳过整个过程。我认为行版本中未更新的列不如更新的列更容易获得,这是不正确的。
马丁·史密斯

@MartinSmith,我们如何进行一种或另一种方式的证明?虽然,以我发现的方式是否可以预测行为可能并不重要。我只是知道,执行相同的SELECT(选择插入,插入和删除之间的联接)并检查字段是否存在实际差异,这是巨大的性能差异,具体取决于WHERE中的字段是否位于UPDATE的SET中。我所看到的行为是一致的,因此是我的理论,但是了解真正的原因将是一件很有趣的事情。我怀疑SET中不在的字段必须返回基表以获取它们的值。
所罗门·鲁兹基

我之前已经看过它的结构。我不记得,如果我发现这样做的一个很好的方式还是我只是用一种很容易地找到能够串并通过穷举tempdbDBCC PAGE
马丁·史密斯

好。在单个文件最小的实例上,tempdb我刚刚尝试了此脚本,将输出粘贴到记事本中并搜索“ EEEEEE”。我在这里的屏幕截图中看到了输出。请注意两行中两列的版本之前和之后。可能有很多简单的方法,但足以满足我的目的!
马丁·史密斯

尽管实际上页面中还有其他较长的EEEEEE字符串,而tempdb不是BBBBBBor 旁边DDDDDD。可能需要做更多调查!虽然这可能是由于REPLICATE通话造成的。
马丁·史密斯

2

我可能会尝试使用if重写

IF EXISTS (SELECT TOP 1 i.CUSTNMBR     
            FROM INSERTED i         
            INNER JOIN DELETED d             
            ON i.CUSTNMBR = d.CUSTNMBR and d.custclass = 'Misc'  
            WHERE d.CUSTCLAS <>i.CUSTCLAS)    
BEGIN

--do your triggerstuff here
END


-1

以下代码可能会提高此触发器的性能。我不知道[custclass]列的正确数据类型,因此您需要对其进行调整。

DECLARE @i AS TABLE (CUSTNMBR VARCHAR(31) NOT NULL PRIMARY KEY, custclass VARCHAR(10) NOT NULL)
DECLARE @d AS TABLE (CUSTNMBR VARCHAR(31) NOT NULL PRIMARY KEY, custclass VARCHAR(10) NOT NULL)
INSERT INTO @i SELECT CUSTNMBR, custclass FROM inserted
INSERT INTO @d SELECT CUSTNMBR, custclass FROM deleted
IF NOT EXISTS
  (SELECT * FROM @i AS i INNER JOIN @d AS d ON d.CUSTNMBR = i.CUSTNMBR
   WHERE i.custclass <> d.custclass) RETURN

请注意,如果触发代码中需要它们,则可以在插入已删除表的内存副本中包括这些附加列。当一次更新多行时,这些表上的主键将大大提高联接性能。祝好运!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.