트리거를 사용한 동기화


11

다음의 이전 토론과 비슷한 요구 사항이 있습니다.

나는 두 개의 테이블을 가지고 [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 원칙을 준수합니까? 삽입이 커밋되었지만 트리거가 실패 할 가능성이 있습니까?

이 질문은 연결된 관련 질문 에서 부분적으로 답변 됩니다. 트리거 코드는 언급 한 ACID 원칙 의 원자 부분을 유지하면서 트리거 된 DML 문과 동일한 트랜잭션 컨텍스트에서 실행됩니다 . 트리거 명령문과 트리거 코드는 모두 하나의 단위로 성공 또는 실패합니다.

ACID 속성은 또한 명시적인 제약 (위반하지 않는 상태로 데이터베이스를 떠나 (트리거 코드 포함) 전체 트랜잭션을 보장 일관된 ) 및 복구 할 수있는 최선을 다하고 효과 (데이터베이스 충돌 살아남을 것입니다 내구성을 ).

주변 (아마 암시 적 또는 자동 커밋) 트랜잭션이 실행되지 않는 SERIALIZABLE격리 수준절연 특성이있다 하지 자동으로 보장. 다른 동시 데이터베이스 활동은 트리거 코드의 올바른 작동을 방해 할 수 있습니다. 예를 들어, 계정 잔액은 다른 세션을 읽은 후 업데이트하기 전에 클래식 경쟁 조건으로 변경 될 수 있습니다.

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.