トリガーを使用した同期


11

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

私は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. IFUPDATE文は奇妙に見えます。正しい[Account]行を更新するより良い方法はありますか?

回答:


13

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

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

ACID特性はまた、明示的な制約(違反していない状態でデータベースを残します(トリガー・コードを含む)全体の取引を保証する一貫性)と任意の回復可能なコミットの効果が(データベースクラッシュ生き残る耐久性を)。

周囲の(おそらく暗黙的または自動コミット)トランザクションが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回実行しました。パフォーマンスは良好で、デッドロックはなく、正しい結果が得られました。私はまだこれを学習演習以外のものとしてお勧めしません。

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