Синхронізація за допомогою тригерів


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. Мої IFта UPDATEзаяви виглядають дивно. Чи є кращий спосіб оновити правильний [Account]рядок?

Відповіді:


13

1. Чи відповідає тригер принципу ACID реляційної бази даних? Чи є якийсь шанс вставити вкладку, але тригер не вдасться?

Частково на це запитання відповідає відповідне запитання, з яким ви пов’язані. Тригер-код виконується в тому ж транзакційному контексті, що і оператор DML, який спричинив його запуск, зберігаючи атомну частину принципів ACID, які ви згадуєте. Оператор запуску та код тригера успішно або не вдається як одиниця.

Ці властивості ACID також гарантувати всю транзакцію (включаючи код запуску) залишать базу даних в стані , яка не порушує будь - які явні обмеження ( Послідовні ) і будь-які добуваються вчинені ефекти зберігаються в разі збій бази даних ( Durable ).

Якщо навколишня (можливо, неявна або автоматична фіксація) транзакція виконується на рівні SERIALIZABLEізоляції , властивість Isolated не гарантується автоматично. Інші паралельні дії бази даних можуть заважати правильній роботі вашого тригерного коду. Наприклад, залишок на рахунку може бути змінений іншим сеансом після того, як ви його прочитали і перед оновленням - класичний стан гонки.

2. Мої заяви IF та UPDATE виглядають дивними. Чи є кращий спосіб оновити правильний рядок [Account]?

Є дуже вагомі причини, що інше питання, з яким ви пов’язані , не пропонує жодного рішення на основі тригера. Тригер-код, призначений для підтримки синхронізованої денормалізованої структури, може бути дуже складним, щоб правильно і правильно перевірити. Навіть дуже просунуті люди 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 , я провів цей тест 100 разів на 32 теми з хорошою продуктивністю, відсутністю тупиків та правильними результатами. Я все ще не рекомендую цього як нічого іншого, крім навчальної вправи.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.