web-dev-qa-db-ja.com

MS SQL Serverで列の変更を検出する最も効率的な方法

私たちのシステムはSQL Server 2000で実行されており、SQL Server 2008へのアップグレードの準備中です。特定の列の変更を検出して、その列を操作する必要があるトリガーコードがたくさんあります変更されました。

明らかに、SQL Serverは PDATE() および COLUMNS_UPDATED() 関数を提供しますが、これらの関数はSQLステートメントに含まれている列not実際に変更された列。

変更された列を判別するには、次のようなコードが必要です(NULLをサポートする列の場合):

IF UPDATE(Col1)
    SELECT @col1_changed = COUNT(*) 
    FROM Inserted i
        INNER JOIN Deleted d ON i.Table_ID = d.Table_ID
    WHERE ISNULL(i.Col1, '<unique null value>') 
            != ISNULL(i.Col1, '<unique null value>')

このコードは、テストに関心のあるすべての列に対して繰り返す必要があります。次に、「変更された」値をチェックして、高価な操作を実行するかどうかを決定できます。もちろん、変更されたすべての行で列の少なくとも1つの値が変更されたことを通知するだけなので、このコード自体に問題があります。

次のような方法で個々のUPDATEステートメントをテストできます。

UPDATE Table SET Col1 = CASE WHEN i.Col1 = d.Col1 
          THEN Col1 
          ELSE dbo.fnTransform(Col1) END
FROM Inserted i
    INNER JOIN Deleted d ON i.Table_ID = d.Table_ID

...しかし、ストアドプロシージャを呼び出す必要がある場合、これはうまく機能しません。そのような場合、私が知る限り、他のアプローチに頼らなければなりません。

私の質問は、変更された行の特定の列の値が実際に変更されたかどうかのトリガーでデータベース操作を予測する問題に対する最良/最も安いアプローチが何であるかについての洞察(またはより良い、ハードデータ)があるかどうかですありません。上記の方法はどちらも理想的とは思えず、より良い方法が存在するかどうか疑問に思いました。

23
mwigdahl

HLGEMは上記のいくつかの良いアドバイスを提供しましたが、それはまさに私が必要としたものではありませんでした。私は過去数日間にわたってかなりのテストを行ってきましたが、これ以上の情報はもう出てこないように見えるので、少なくともここで結果を共有すると思いました。

システムのプライマリテーブルのいずれかの事実上狭いサブセット(9列)であるテーブルを設定し、テーブルの実稼働バージョンと同じ深さになるように実稼働データを入力しました。

その後、そのテーブルを複製し、最初のトリガーで個々の列の変更をすべて検出しようとするトリガーを作成し、その列のデータが実際に変更されたかどうかに基づいて各列の更新を予測しました。

2番目のテーブルについては、広範な条件付きCASEロジックを使用して単一のステートメント内のすべての列に対するすべての更新を行うトリガーを作成しました。

次に、4つのテストを実行しました。

  1. 単一行への単一列の更新
  2. 10000行への単一列の更新
  3. 単一行への9列の更新
  4. 10000行への9列の更新

テーブルのインデックス付きバージョンとインデックスなしバージョンの両方でこのテストを繰り返し、SQL 2000およびSQL 2008サーバーですべてを繰り返しました。

私が得た結果はかなり興味深いものでした。

2番目の方法(SET句にヘアリーCASEロジックを含む1つの単一の更新ステートメント)は、単一列の変更が影響するという単一の例外を除いて、個々の変更検出(テストに応じて多かれ少なかれ)よりも均一に優れたパフォーマンスを発揮しましたSQL 2000で実行されている、列にインデックスが付けられた行が多数あります。この特定のケースでは、このような狭くて深い更新はあまり行いません。


私の結論が私が疑うほど普遍的であるか、それとも特定の構成に固有であるかどうかを確認するために、他の人の同様のタイプのテストの結果を聞くことに興味があります。

始めるために、ここに私が使用したテストスクリプトを示します。明らかに、他のデータを入力してデータを取り込む必要があります。

create table test1
( 
    t_id int NOT NULL PRIMARY KEY,
    i1 int NULL,
    i2 int NULL,
    i3 int NULL,
    v1 varchar(500) NULL,
    v2 varchar(500) NULL,
    v3 varchar(500) NULL,
    d1 datetime NULL,
    d2 datetime NULL,
    d3 datetime NULL
)

create table test2
( 
    t_id int NOT NULL PRIMARY KEY,
    i1 int NULL,
    i2 int NULL,
    i3 int NULL,
    v1 varchar(500) NULL,
    v2 varchar(500) NULL,
    v3 varchar(500) NULL,
    d1 datetime NULL,
    d2 datetime NULL,
    d3 datetime NULL
)

-- optional indexing here, test with it on and off...
CREATE INDEX [IX_test1_i1] ON [dbo].[test1] ([i1])
CREATE INDEX [IX_test1_i2] ON [dbo].[test1] ([i2])
CREATE INDEX [IX_test1_i3] ON [dbo].[test1] ([i3])
CREATE INDEX [IX_test1_v1] ON [dbo].[test1] ([v1])
CREATE INDEX [IX_test1_v2] ON [dbo].[test1] ([v2])
CREATE INDEX [IX_test1_v3] ON [dbo].[test1] ([v3])
CREATE INDEX [IX_test1_d1] ON [dbo].[test1] ([d1])
CREATE INDEX [IX_test1_d2] ON [dbo].[test1] ([d2])
CREATE INDEX [IX_test1_d3] ON [dbo].[test1] ([d3])

CREATE INDEX [IX_test2_i1] ON [dbo].[test2] ([i1])
CREATE INDEX [IX_test2_i2] ON [dbo].[test2] ([i2])
CREATE INDEX [IX_test2_i3] ON [dbo].[test2] ([i3])
CREATE INDEX [IX_test2_v1] ON [dbo].[test2] ([v1])
CREATE INDEX [IX_test2_v2] ON [dbo].[test2] ([v2])
CREATE INDEX [IX_test2_v3] ON [dbo].[test2] ([v3])
CREATE INDEX [IX_test2_d1] ON [dbo].[test2] ([d1])
CREATE INDEX [IX_test2_d2] ON [dbo].[test2] ([d2])
CREATE INDEX [IX_test2_d3] ON [dbo].[test2] ([d3])

insert into test1 (t_id, i1, i2, i3, v1, v2, v3, d1, d2, d3)
-- add data population here...

insert into test2 (t_id, i1, i2, i3, v1, v2, v3, d1, d2, d3)
select t_id, i1, i2, i3, v1, v2, v3, d1, d2, d3 from test1

go

create trigger test1_update on test1 for update
as
begin

declare @i1_changed int,
    @i2_changed int,
    @i3_changed int,
    @v1_changed int,
    @v2_changed int,
    @v3_changed int,
    @d1_changed int,
    @d2_changed int,
    @d3_changed int

IF UPDATE(i1)
    SELECT @i1_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.i1,0) != ISNULL(d.i1,0)
IF UPDATE(i2)
    SELECT @i2_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.i2,0) != ISNULL(d.i2,0)
IF UPDATE(i3)
    SELECT @i3_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.i3,0) != ISNULL(d.i3,0)
IF UPDATE(v1)
    SELECT @v1_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.v1,'') != ISNULL(d.v1,'')
IF UPDATE(v2)
    SELECT @v2_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.v2,'') != ISNULL(d.v2,'')
IF UPDATE(v3)
    SELECT @v3_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.v3,'') != ISNULL(d.v3,'')
IF UPDATE(d1)
    SELECT @d1_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.d1,'1/1/1980') != ISNULL(d.d1,'1/1/1980')
IF UPDATE(d2)
    SELECT @d2_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.d2,'1/1/1980') != ISNULL(d.d2,'1/1/1980')
IF UPDATE(d3)
    SELECT @d3_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d
        ON i.t_id = d.t_id WHERE ISNULL(i.d3,'1/1/1980') != ISNULL(d.d3,'1/1/1980')

if (@i1_changed > 0)
begin
    UPDATE test1 SET i1 = CASE WHEN i.i1 > d.i1 THEN i.i1 ELSE d.i1 END
    FROM test1
        INNER JOIN inserted i ON test1.t_id = i.t_id
        INNER JOIN deleted d ON i.t_id = d.t_id
    WHERE i.i1 != d.i1
end

if (@i2_changed > 0)
begin
    UPDATE test1 SET i2 = CASE WHEN i.i2 > d.i2 THEN POWER(i.i2, 1.1) ELSE POWER(d.i2, 1.1) END
    FROM test1
        INNER JOIN inserted i ON test1.t_id = i.t_id
        INNER JOIN deleted d ON i.t_id = d.t_id
    WHERE i.i2 != d.i2
end

if (@i3_changed > 0)
begin
    UPDATE test1 SET i3 = i.i3 ^ d.i3
    FROM test1
        INNER JOIN inserted i ON test1.t_id = i.t_id
        INNER JOIN deleted d ON i.t_id = d.t_id
    WHERE i.i3 != d.i3
end

if (@v1_changed > 0)
begin
    UPDATE test1 SET v1 = i.v1 + 'a'
    FROM test1
        INNER JOIN inserted i ON test1.t_id = i.t_id
        INNER JOIN deleted d ON i.t_id = d.t_id
    WHERE i.v1 != d.v1
end

UPDATE test1 SET v2 = LEFT(i.v2, 5) + '|' + RIGHT(d.v2, 5)
FROM test1
    INNER JOIN inserted i ON test1.t_id = i.t_id
    INNER JOIN deleted d ON i.t_id = d.t_id

if (@v3_changed > 0)
begin
    UPDATE test1 SET v3 = LEFT(i.v3, 5) + '|' + LEFT(i.v2, 5) + '|' + LEFT(i.v1, 5)
    FROM test1
        INNER JOIN inserted i ON test1.t_id = i.t_id
        INNER JOIN deleted d ON i.t_id = d.t_id
    WHERE i.v3 != d.v3
end

if (@d1_changed > 0)
begin
    UPDATE test1 SET d1 = DATEADD(dd, 1, i.d1)
    FROM test1
        INNER JOIN inserted i ON test1.t_id = i.t_id
        INNER JOIN deleted d ON i.t_id = d.t_id
    WHERE i.d1 != d.d1
end

if (@d2_changed > 0)
begin
    UPDATE test1 SET d2 = DATEADD(dd, DATEDIFF(dd, i.d2, d.d2), d.d2)
    FROM test1
        INNER JOIN inserted i ON test1.t_id = i.t_id
        INNER JOIN deleted d ON i.t_id = d.t_id
    WHERE i.d2 != d.d2
end

UPDATE test1 SET d3 = DATEADD(dd, 15, i.d3)
FROM test1
    INNER JOIN inserted i ON test1.t_id = i.t_id
    INNER JOIN deleted d ON i.t_id = d.t_id

end

go

create trigger test2_update on test2 for update
as
begin

    UPDATE test2 SET
        i1 = 
            CASE
            WHEN ISNULL(i.i1, 0) != ISNULL(d.i1, 0)
            THEN CASE WHEN i.i1 > d.i1 THEN i.i1 ELSE d.i1 END
            ELSE test2.i1 END,
        i2 = 
            CASE
            WHEN ISNULL(i.i2, 0) != ISNULL(d.i2, 0)
            THEN CASE WHEN i.i2 > d.i2 THEN POWER(i.i2, 1.1) ELSE POWER(d.i2, 1.1) END
            ELSE test2.i2 END,
        i3 = 
            CASE
            WHEN ISNULL(i.i3, 0) != ISNULL(d.i3, 0)
            THEN i.i3 ^ d.i3
            ELSE test2.i3 END,
        v1 = 
            CASE
            WHEN ISNULL(i.v1, '') != ISNULL(d.v1, '')
            THEN i.v1 + 'a'
            ELSE test2.v1 END,
        v2 = LEFT(i.v2, 5) + '|' + RIGHT(d.v2, 5),
        v3 = 
            CASE
            WHEN ISNULL(i.v3, '') != ISNULL(d.v3, '')
            THEN LEFT(i.v3, 5) + '|' + LEFT(i.v2, 5) + '|' + LEFT(i.v1, 5)
            ELSE test2.v3 END,
        d1 = 
            CASE
            WHEN ISNULL(i.d1, '1/1/1980') != ISNULL(d.d1, '1/1/1980')
            THEN DATEADD(dd, 1, i.d1)
            ELSE test2.d1 END,
        d2 = 
            CASE
            WHEN ISNULL(i.d2, '1/1/1980') != ISNULL(d.d2, '1/1/1980')
            THEN DATEADD(dd, DATEDIFF(dd, i.d2, d.d2), d.d2)
            ELSE test2.d2 END,
        d3 = DATEADD(dd, 15, i.d3)
    FROM test2
        INNER JOIN inserted i ON test2.t_id = i.t_id
        INNER JOIN deleted d ON test2.t_id = d.t_id

end

go

-----
-- the below code can be used to confirm that the triggers operated identically over both tables after a test
select top 10 test1.i1, test2.i1, test1.i2, test2.i2, test1.i3, test2.i3, test1.v1, test2.v1, test1.v2, test2.v2, test1.v3, test2.v3, test1.d1, test1.d1, test1.d2, test2.d2, test1.d3, test2.d3
from test1 inner join test2 on test1.t_id = test2.t_id
where 
    test1.i1 != test2.i1 or 
    test1.i2 != test2.i2 or
    test1.i3 != test2.i3 or
    test1.v1 != test2.v1 or 
    test1.v2 != test2.v2 or
    test1.v3 != test2.v3 or
    test1.d1 != test2.d1 or 
    test1.d2 != test2.d2 or
    test1.d3 != test2.d3

-- test 1 -- one column, one row
update test1 set i3 = 64 where t_id = 1000
go
update test2 set i3 = 64 where t_id = 1000
go

update test1 set i3 = 64 where t_id = 1001
go
update test2 set i3 = 64 where t_id = 1001
go

-- test 2 -- one column, 10000 rows
update test1 set v3 = LEFT(v3, 50) where t_id between 10000 and 20000
go
update test2 set v3 = LEFT(v3, 50) where t_id between 10000 and 20000
go

-- test 3 -- all columns, 1 row, non-self-referential
update test1 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL
where t_id = 3000
go
update test2 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL
where t_id = 3000
go

-- test 4 -- all columns, 10000 rows, non-self-referential
update test1 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL
where t_id between 30000 and 40000
go
update test2 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL
where t_id between 30000 and 40000
go

-----

drop table test1
drop table test2
7
mwigdahl

私は決してやらないから始めましょう、トリガーのストアドプロシージャを決して呼び出さないということです。複数行の挿入に対応するには、procをカーソルで移動する必要があります。つまり、セットベースのクエリ(たとえば、すべての価格を10%更新)でロードした200,000行は、トリガーがロードを処理しようと積極的に試みるため、テーブルを数時間ロックする可能性があります。さらに、プロシージャで何かが変更された場合、テーブルへの挿入をまったく中断するか、テーブルを完全にハングアップさせることさえできます。私は、トリガーコードがトリガー以外に何も呼び出さないことを確信しています。

個人的には、単に自分のタスクを実行することを好みます。トリガーで適切に実行するアクションを記述した場合、列が変更された場所のみを更新、削除、または挿入します。

例:パフォーマンス上の理由で非正規化が行われたため、2つの場所に格納しているlast_nameフィールドを更新するとします。

update t
set lname = i.lname
from table2 t 
join inserted i on t.fkfield = i.pkfield
where t.lname <>i.lname

ご覧のとおり、更新するテーブルに現在あるものとは異なるlnameのみが更新されます。

監査を行い、変更された行のみを記録する場合は、i.field1 <> d.field1またはi.field2 <> d.field3などのすべてのフィールドを使用して比較を行います(すべてのフィールドを使用するなど)

18
HLGEM

EXCEPT演算子を使用して調査することをお勧めします。変更されていない行を取り除くことができるセットベースの演算子です。良いことは、EXCEPT演算子の前にリストされた最初のセットの行を検索し、EXCEPTの後にリストされた2番目の行ではなく、NULL値を等しいと見なすことです。

_WITH ChangedData AS (
SELECT d.Table_ID , d.Col1 FROM deleted d
EXCEPT 
SELECT i.Table_ID , i.Col1  FROM inserted i
)
/*Do Something with the ChangedData */
_

これは、トリガーでISNULL()を使用せずにNullを許可する列の問題を処理し、変更を検出するためのNiceセットベースのアプローチでcol1に変更がある行のIDのみを返します。アプローチをテストしていませんが、時間をかけるだけの価値があるかもしれません。 EXCEPTはSQL Server 2005で導入されたと思います。

10
Todd

上記のTodd/arghtypeで述べたように、EXCEPTセット演算子を使用することをお勧めします。

「挿入済み」を「削除済み」の前に置いて、INSERTとUPDATEが検出されるようにするため、この回答を追加しました。したがって、通常、挿入と更新の両方をカバーするトリガーを1つ持つことができます。 OR(NOT EXISTS(SELECT * FROM Inserted)AND EXISTS(SELECT * FROM deleted))を追加して削除を検出することもできます

指定された列のみで値が変更されたかどうかを判断します。私は他のソリューションと比較したパフォーマンスを調査していませんが、私のデータベースではうまく機能しています。

EXCEPTセット演算子を使用して、右側のクエリにも見つからない左側のクエリの行を返します。このコードは、INSERT、UPDATE、およびDELETEトリガーで使用できます。

「PKID」列は主キーです。 2つのセット間のマッチングを有効にする必要があります。主キーに複数の列がある場合は、挿入されたセットと削除されたセットを正しく一致させるためにすべての列を含める必要があります。

-- Only do trigger logic if specific field values change.
IF EXISTS(SELECT  PKID
                ,Column1
                ,Column7
                ,Column10
          FROM inserted
          EXCEPT
          SELECT PKID
                ,Column1
                ,Column7
                ,Column10
          FROM deleted )    -- Tests for modifications to fields that we are interested in
OR (NOT EXISTS(SELECT * FROM inserted) AND EXISTS(SELECT * FROM deleted)) -- Have a deletion
BEGIN
          -- Put code here that does the work in the trigger

END

変更された行を後続のトリガーロジックで使用する場合、通常、EXCEPTクエリの結果を後で参照できるテーブル変数に入れます。

これが興味深いことを願っています:-)

6
David Coster

SQL Server 2008には、変更追跡のための別の手法があります。

変更データキャプチャと変更追跡の比較

4
Serg