web-dev-qa-db-ja.com

トリガーでINSERTEDテーブルとDELETEDテーブルを結合する恐ろしいパフォーマンス

特定の列が特定の値から他の値に変更されるのを監視するテーブルに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になります。プロファイラーでこのステートメントが数分間停止するのを見ることができます。

実行計画は奇妙です。挿入されたスキャンが3,714回実行され、出力行が約1,850万行あります。 CUSTCLAS列のフィルターを通過します。これは(ネストされたループを介して)削除されたスキャン(これもCUSTCLASでフィルター処理されます)に結合されます。このスキャンは1回だけ実行され、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
12
db2

明示的な_INNER MERGE JOIN_または_INNER HASH JOIN_ヒ​​ントを使用して評価することもできますが、おそらくこれらのテーブルをトリガーの後半で再度使用している場合、insertedおよびdeletedテーブルの内容をインデックス付き_#temp_テーブルとそれで終わりです。

それらは自動的に作成される有用なインデックスを取得しません。

10
Martin Smith

私はこれが答えられたことを知っていますが、最近アクティブになってポップアップしただけで、何百万もの行があるテーブルでもこれに遭遇しました。受け入れられた回答を無視するわけではありませんが、同様のテスト(1つ以上の列の値が実際に変更されているかどうかを確認する)を行うときのトリガーパフォーマンスの重要な要素は列かどうかであるという経験から、テストされているのは、実際にはUPDATEステートメントの一部です。実際にinsertedステートメントの一部であるnot部分であるdeletedテーブルとUPDATEテーブルの間の列を比較すると、これらのフィールドがUPDATEステートメント(実際に変更される値に関係なく)。それらの列のいずれかが変更されている可能性を論理的に排除できる場合、変更があったかどうかを判断するためにそのすべての作業(つまり、X行のNフィールドを比較するクエリ)を行う理由(それらが存在しない場合は明らかに不可能) SETステートメントのUPDATE句内。

私が採用した解決策は、トリガー内でのみ機能する PDATE() 関数を使用することでした。この組み込み関数は、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. 関連するフィールドの少なくとも1つがSETUPDATE句にありおよび1つの行のそれらの列の少なくとも1つが変更されている

NOT (UPDATE...) OR NOT EXISTS()は奇妙に見えたり、逆に見えるかもしれませんが、関連する列がSELECTの一部ではない場合、insertedおよびdeletedテーブルでUPDATEを実行しないように設計されています。

必要に応じて、 COLUMNS_UPDATED() 関数は、どの列がUPDATEステートメントの一部であるかを決定する別のオプションです。

10
Solomon Rutzky

存在する場合は、

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
2
HLGEM

http://dave.brittens.org/blog/writing-well-behaved-triggers.html

Daveによると、仮想INSERTED/DELETEDテーブルには何もないため、一時テーブルまたはインデックス付きのテーブル変数を使用する必要があります。再帰的なトリガーの可能性がある場合は、テーブルの変数を使用して名前の衝突を回避する必要があります。

元の投稿がかなり前にあったため、誰かがこれが役立つことを願っています...

1
Keith