Synchronisation mit Triggern


11

Ich habe eine ähnliche Anforderung wie in früheren Diskussionen unter:

Ich habe zwei Tabellen [Account].[Balance]und [Transaction].[Amount]:

CREATE TABLE Account (
      AccountID    INT
    , Balance      MONEY
);

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

Wenn die [Transaction]Tabelle eingefügt, aktualisiert oder gelöscht wird , [Account].[Balance]sollte die Tabelle basierend auf der Tabelle aktualisiert werden [Amount].

Derzeit habe ich einen Auslöser für diesen Job:

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

Obwohl dies zu funktionieren scheint, habe ich Fragen:

  1. Entspricht der Trigger dem ACID-Prinzip der relationalen Datenbank? Gibt es eine Möglichkeit, dass eine Einfügung festgeschrieben wird, der Auslöser jedoch fehlschlägt?
  2. Meine IFund UPDATEAussagen sehen seltsam aus. Gibt es eine bessere Möglichkeit, die richtige [Account]Zeile zu aktualisieren ?

Antworten:


13

1. Folgt der Trigger dem ACID-Prinzip der relationalen Datenbank? Gibt es eine Möglichkeit, dass eine Einfügung festgeschrieben wird, der Auslöser jedoch fehlschlägt?

Diese Frage wird teilweise in einer verwandten Frage beantwortet, auf die Sie verlinkt haben. Der Triggercode wird im selben Transaktionskontext wie die DML-Anweisung ausgeführt, die ihn ausgelöst hat, wobei der atomare Teil der von Ihnen erwähnten ACID-Prinzipien erhalten bleibt. Die auslösende Anweisung und der Triggercode sind als Einheit erfolgreich oder fehlgeschlagen.

Die ACID-Eigenschaften garantieren auch, dass die gesamte Transaktion (einschließlich des Triggercodes) die Datenbank in einem Zustand verlässt, der keine expliziten Einschränkungen verletzt ( konsistent ), und alle wiederherstellbaren festgeschriebenen Effekte überleben einen Datenbankabsturz ( dauerhaft ).

Sofern die umgebende (möglicherweise implizite oder automatische Festschreibung) Transaktion nicht auf der SERIALIZABLEIsolationsstufe ausgeführt wird , wird die isolierte Eigenschaft nicht automatisch garantiert. Andere gleichzeitige Datenbankaktivitäten können den korrekten Betrieb Ihres Triggercodes beeinträchtigen. Zum Beispiel könnte der Kontostand durch eine andere Sitzung geändert werden, nachdem Sie ihn gelesen und aktualisiert haben - eine klassische Rennbedingung.

2. Meine IF- und UPDATE-Anweisungen sehen seltsam aus. Gibt es eine bessere Möglichkeit, die richtige Zeile [Konto] zu aktualisieren?

Es gibt sehr gute Gründe, warum die andere Frage, mit der Sie verlinkt haben, keine triggerbasierten Lösungen bietet. Triggercode, der entwickelt wurde, um eine denormalisierte Struktur synchron zu halten, kann äußerst schwierig sein , das Richtige zu finden und richtig zu testen. Selbst sehr fortgeschrittene SQL Server-Leute mit langjähriger Erfahrung haben damit zu kämpfen.

Die Aufrechterhaltung einer guten Leistung bei gleichzeitiger Wahrung der Korrektheit in allen Szenarien und die Vermeidung von Problemen wie Deadlocks erhöht die Schwierigkeit zusätzlich. Ihr Triggercode ist bei weitem nicht robust und aktualisiert den Kontostand jedes Kontos, selbst wenn nur eine einzige Transaktion geändert wird. Bei einer Trigger-basierten Lösung gibt es alle möglichen Risiken und Herausforderungen, wodurch die Aufgabe für jemanden, der in diesem Technologiebereich relativ neu ist, zutiefst ungeeignet ist.

Um einige der Probleme zu veranschaulichen, zeige ich unten einen Beispielcode. Dies ist keine streng getestete Lösung (Auslöser sind schwer!), Und ich schlage nicht vor, dass Sie sie als etwas anderes als eine Lernübung verwenden. Für ein reales System haben die Nicht-Trigger-Lösungen wichtige Vorteile. Sie sollten daher die Antworten auf die andere Frage sorgfältig prüfen und die Trigger-Idee vollständig vermeiden.

Beispieltabellen

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)
);

Verhindern TRUNCATE TABLE

Trigger werden nicht von ausgelöst TRUNCATE TABLE. Die folgende leere Tabelle dient lediglich dazu, das TransactionsAbschneiden der Tabelle zu verhindern (ein Verweis durch einen Fremdschlüssel verhindert das Abschneiden der Tabelle):

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)
);

Trigger Definition

Der folgende Triggercode stellt sicher, dass nur die erforderlichen Kontoeinträge verwaltet werden, und verwendet SERIALIZABLEdort die Semantik. Als wünschenswerter Nebeneffekt werden auch die falschen Ergebnisse vermieden, die auftreten können , wenn eine Isolationsstufe für die Zeilenversionierung verwendet wird. Der Code vermeidet auch die Ausführung des Triggercodes, wenn keine Zeilen von der Quellanweisung betroffen waren. Die temporäre Tabelle und der RECOMPILEHinweis werden verwendet, um Probleme mit dem Ausführungsplan des Auslösers zu vermeiden, die durch ungenaue Kardinalitätsschätzungen verursacht werden:

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;

Testen

Der folgende Code verwendet eine Zahlentabelle , um 100.000 Konten mit einem Saldo von Null zu erstellen:

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

Der folgende Testcode fügt 10.000 zufällige Transaktionen ein:

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;

Mit dem SQLQueryStress- Tool habe ich diesen Test 100 Mal auf 32 Threads mit guter Leistung, ohne Deadlocks und korrekten Ergebnissen ausgeführt. Ich empfehle dies immer noch nicht als etwas anderes als eine Lernübung.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.