トリガーで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がかかります。プロファイラーでこのステートメントが数分間停止するのを見ることができます。

実行計画は奇妙です。挿入されたスキャンが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

「TOP 1」を取り除くことはできますか?単一のケースがあるかどうかを確認するだけの場合は必要ないかもしれないオーバーヘッドが発生していると思います...
JHFB

回答:


10

明示的INNER MERGE JOINまたはINNER HASH JOINヒントを使用して評価することもできますが、おそらくこれらのテーブルをトリガーの後半で再び使用していることを考えると、インデックス付きテーブルにinserteddeletedテーブルのコンテンツを挿入し、それで処理するほうがよいでしょう#temp

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


さて、これはそれを大幅にスピードアップしますが、トリガーの実行をカスケードする可能性があります。各トリガーで同じ一時テーブル名(#i、#d)を使用すると、競合します。すべてのトリガーで異なる一時テーブル名を使用するよりも優れた/安全なソリューションはありますか?
db2 2012年

テーブル変数を使用して評価し(CUSTNMBR一意のクラスター化インデックスを作成するためにに定義された主キーを使用)、OPTION (RECOMPILE)ヒントを使用して行数を考慮に入れるか、または次のような特定の命名規則を使用します#i_dbo_YourTable
Martin Smith

私はそれらにのような名前を付けることで落ち着くと思います#trigger_name_i。テーブル変数を使用する場合、明示的なCREATE TABLEを使用してコードをさらに整理する必要があります。カスケードトリガーはありますが、再帰トリガーはありません。そのため、私は安全だと思います...
db2

この目的のために、一時テーブルではなくテーブル変数をお勧めします。テーブル変数は依然としてプライマリおよびセカンダリ(一意の)インデックスを持つことができ、トリガーが終了すると自動的にクリーンアップされ、テーブル変数がそのトリガーの実行のみにスコープ指定されます(同じ名前の他のテーブル変数と競合しません)呼び出しスタック)。テーブル定義コードのオーバーヘッドを節約するには、それぞれにテーブルタイプを定義し、タイプ名を使用してテーブル変数を宣言します。
Chris Smith、

@ChrisSmithも必要になることが多いOPTION (RECOMPILE)ので、カーディナリティが考慮されます。
マーティン・スミス

10

私はこれが答えられたことを知っていますが、最近アクティブになってポップアップしただけで、何百万もの行があるテーブルでもこれに遭遇しました。受け入れられた回答を無視するわけではありませんが、同様のテスト(1つ以上の列で実際に値が変更されているかどうかを確認する)を行うときのトリガーパフォーマンスの重要な要素は列であるかどうかであることが私の経験からわかっています。テストされていることは実際には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()奇数または後方に見えるかもしれないが、やって回避するように設計されてSELECTinserteddeleted関連する列のどれもの一部でない場合、テーブルUPDATE

必要に応じて、COLUMNS_UPDATED()関数は、UPDATEステートメントの一部である列を判別するためのもう1つのオプションです。


1
チェックしてUPDATE(CUSTCLAS)、偽(+1)の場合はすべてをスキップする必要があるというのが良い点です。更新されていない列は、更新された列ほど簡単には行バージョンで利用できないというのはあなたの考えではないと思います。
マーティン・スミス

@MartinSmith、どうやってそれを証明するのですか?しかし、私が見つけた方法で動作が予測可能かどうかは問題ではないかもしれません。WHEREのフィールドがUPDATEのSETにあるかどうかに応じて、INSERTEDとDELETEDの間で同じSELECT、JOINを実行し、フィールドの実際の違いをチェックすることは、パフォーマンスの大幅な違いであることを知っています。私が見た行動は一貫しているため、私の理論ですが、本当の理由を知ることは良い/興味深いでしょう。SETにないフィールドは、値のためにベーステーブルに戻る必要があるのではないかと思いました。
ソロモンRutzky

これの構造は以前に見たことがあります。私はそれを行うための良い方法を見つけた場合、私は思い出すことができないか、私は簡単にできる文字列とを通して徹底的な検索を見つける使用tempdbしてDBCC PAGE
マーティン・スミス

OK。最小サイズの単一ファイルのインスタンスで、このスクリプトをtempdb試し、出力をメモ帳に貼り付け、「EEEEEE」を検索しました。ここのスクリーンショットに出力が表示されます。両方の行の両方の列の前と後のバージョンに注意してください。もっと簡単な方法があるかもしれませんが、ここでは私の目的には十分です!
マーティン・スミス

実際にはtempdbBBBBBBまたはの隣ではないページに他の長いEEEEEE文字列がありますDDDDDD。さらに調査が必要な場合があります。多分これはREPLICATE呼び出しによるものです。
マーティン・スミス

2

存在する場合は、

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

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

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

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


-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

トリガーコードで必要な場合は、挿入および削除されたテーブルのメモリコピーにこれらの列を追加できます。これらのテーブルの主キーを使用すると、一度に複数の行を更新する場合の結合パフォーマンスが大幅に向上します。幸運を!

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.