web-dev-qa-db-ja.com

トリガーを使用した同期

私は以前の議論と同様の要件を持っています:

2つのテーブル[Account].[Balance][Transaction].[Amount]があります。

CREATE TABLE Account (
      AccountID    INT
    , Balance      MONEY
);

CREATE TABLE Transaction (
      TransactionID INT
     , AccountID    INT
    , Amount      MONEY
);

[Transaction]テーブルに対して挿入、更新、または削除がある場合、[Account].[Balance][Amount]に基づいて更新する必要があります。

現在、私はこの仕事をするためのトリガーを持っています:

ALTER TRIGGER [dbo].[TransactionChanged] 
ON  [dbo].[Transaction]
AFTER INSERT, UPDATE, DELETE
AS 
BEGIN
IF  EXISTS (select 1 from [Deleted]) OR EXISTS (select 1 from [Inserted])
    UPDATE [dbo].[Account]
    SET
    [Account].[Balance] = [Account].[Balance] + 
        (
            Select ISNULL(Sum([Inserted].[Amount]),0)
            From [Inserted] 
            Where [Account].[AccountID] = [Inserted].[AccountID]
        )
        -
        (
            Select ISNULL(Sum([Deleted].[Amount]),0)
            From [Deleted] 
            Where [Account].[AccountID] = [Deleted].[AccountID]
        )
END

これは機能しているようですが、質問があります:

  1. トリガーはリレーショナルデータベースのACID原則に従いますか?挿入がコミットされる可能性はありますが、トリガーは失敗しますか?
  2. 私のIFおよびUPDATEステートメントは奇妙に見えます。正しい[Account]行を更新するより良い方法はありますか?
11
Yiping

1。トリガーはリレーショナルデータベースのACID原則に従いますか?挿入がコミットされてもトリガーが失敗する可能性はありますか?

この質問は、あなたがリンクしている 関連する質問 で部分的に回答されています。トリガーコードは、起動する原因となったDMLステートメントと同じトランザクションコンテキストで実行され、言及するACID原則のAtomic部分を保持します。トリガーステートメントとトリガーコードの両方が1つのユニットとして成功または失敗します。

ACIDプロパティ は、トランザクション全体(トリガーコードを含む)がデータベースを明示的な制約に違反しない状態のままにすることも保証します(Consistent)と、回復可能なコミットされた効果は、データベースのクラッシュ(Durable)を乗り切ります。

周囲の(おそらく暗黙的または自動コミット)トランザクションが SERIALIZABLE分離レベル で実行されていない限り、Isolatedプロパティは自動的には保証されません。他の同時データベースアクティビティは、トリガーコードの正しい動作を妨げる可能性があります。たとえば、アカウント残高は、それを読んだ後、更新する前に別のセッションによって変更される可能性があります。これは、古典的な競合状態です。

2。私のIFおよびUPDATEステートメントは奇妙に見えます。正しい[アカウント]行を更新するより良い方法はありますか?

非常に良い理由があります あなたがリンクした他の質問 はトリガーベースのソリューションを提供していません。非正規化された構造の同期を維持するように設計されたトリガーコードは、適切にテストして適切にテストするために非常にトリッキーになる可能性があります。長年の経験を持つ非常に高度なSQL Serverの人々でさえ、これに苦労しています。

すべてのシナリオで正確性を維持すると同時に優れたパフォーマンスを維持し、デッドロックなどの問題を回避すると、さらに困難な側面が追加されます。トリガーコードは堅牢でどこにも近く、単一のトランザクションのみが変更された場合でも、すべてのアカウントの残高を更新します。トリガーベースのソリューションにはあらゆる種類のリスクと課題があり、このテクノロジー領域に比較的慣れていない人には、このタスクは非常に不適切です。

問題の一部を説明するために、以下にサンプルコードをいくつか示します。これは厳密にテストされたソリューションではありません(トリガーは難しいです!)。これを学習以外の目的で使用することはお勧めしません。実際のシステムでは、非トリガーソリューションには重要な利点があるため、 他の質問 に対する回答を注意深く検討し、トリガーの考えを完全に回避する必要があります。

サンプルテーブル

CREATE TABLE dbo.Accounts
(
    AccountID integer NOT NULL,
    Balance money NOT NULL,

    CONSTRAINT PK_Accounts_ID
    PRIMARY KEY CLUSTERED (AccountID)
);

CREATE TABLE dbo.Transactions
(
    TransactionID integer IDENTITY NOT NULL,
    AccountID integer NOT NULL,
    Amount money NOT NULL,

    CONSTRAINT PK_Transactions_ID
    PRIMARY KEY CLUSTERED (TransactionID),

    CONSTRAINT FK_Accounts
    FOREIGN KEY (AccountID)
    REFERENCES dbo.Accounts (AccountID)
);

防止TRUNCATE TABLE

トリガーはTRUNCATE TABLEによって起動されません。次の空のテーブルは、純粋にTransactionsテーブルが切り捨てられるのを防ぐために存在します(外部キーによって参照されているため、テーブルの切り捨てが行われません)。

CREATE TABLE dbo.PreventTransactionsTruncation
(
    Dummy integer NULL,

    CONSTRAINT FK_Transactions
    FOREIGN KEY (Dummy)
    REFERENCES dbo.Transactions (TransactionID),

    CONSTRAINT CHK_NoRows
    CHECK (Dummy IS NULL AND Dummy IS NOT NULL)
);

トリガーの定義

次のトリガーコードは、必要なアカウントエントリのみが維持されるようにし、SERIALIZABLEセマンティクスを使用します。望ましい副作用として、これは、行バージョンの分離レベルが使用されている場合に発生する可能性がある 誤った結果 も回避します。また、ソースステートメントの影響を受ける行がない場合、コードはトリガーコードの実行を回避します。一時テーブルとRECOMPILEヒントは、不正確なカーディナリティの推定によって引き起こされるトリガー実行プランの問題を回避するために使用されます。

CREATE TRIGGER dbo.TransactionChange ON dbo.Transactions 
AFTER INSERT, UPDATE, DELETE 
AS
BEGIN
IF @@ROWCOUNT = 0 OR
    TRIGGER_NESTLEVEL
    (
        OBJECT_ID(N'dbo.TransactionChange', N'TR'),
        'AFTER', 
        'DML'
    ) > 1 
    RETURN;

    SET NOCOUNT, XACT_ABORT ON;

    CREATE TABLE #Delta
    (
        AccountID integer PRIMARY KEY,
        Amount money NOT NULL
    );

    INSERT #Delta
        (AccountID, Amount)
    SELECT 
        InsDel.AccountID,
        Amount = SUM(InsDel.Amount)
    FROM 
    (
        SELECT AccountID, Amount
        FROM Inserted
        UNION ALL
        SELECT AccountID, $0 - Amount
        FROM Deleted
    ) AS InsDel
    GROUP BY
        InsDel.AccountID;

    UPDATE A
    SET Balance += D.Amount
    FROM #Delta AS D
    JOIN dbo.Accounts AS A WITH (SERIALIZABLE)
        ON A.AccountID = D.AccountID
    OPTION (RECOMPILE);
END;

テスト中

次のコードは 数値のテーブル を使用して、残高がゼロの100,000アカウントを作成します。

INSERT dbo.Accounts
    (AccountID, Balance)
SELECT
    N.n, $0
FROM dbo.Numbers AS N
WHERE
    N.n BETWEEN 1 AND 100000;

以下のテストコードは、10,000のランダムトランザクションを挿入します。

INSERT dbo.Transactions
    (AccountID, Amount)
SELECT 
    CONVERT(integer, Rand(CHECKSUM(NEWID())) * 100000 + 1),
    CONVERT(money, Rand(CHECKSUM(NEWID())) * 500 - 250)
FROM dbo.Numbers AS N
WHERE 
    N.n BETWEEN 1 AND 10000;

SQLQueryStress ツールを使用して、このテストを32スレッドで100回実行しました。パフォーマンスは良好で、デッドロックはなく、正しい結果が得られました。私はまだこれを学習演習以外のものとしてお勧めしません。

13
Paul White 9