Sincronizzazione tramite trigger


11

Ho un requisito simile alle discussioni precedenti su:

Ho due tavoli [Account].[Balance]e [Transaction].[Amount]:

CREATE TABLE Account (
      AccountID    INT
    , Balance      MONEY
);

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

Quando è presente un inserimento, aggiornamento o eliminazione rispetto alla [Transaction]tabella, è [Account].[Balance]necessario aggiornarlo in base a [Amount].

Attualmente ho un grilletto per fare questo lavoro:

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

Anche se questo sembra funzionare, ho delle domande:

  1. Il trigger segue il principio ACID del database relazionale? È possibile che venga inserito un inserimento ma il trigger non riesce?
  2. Le mie IFe le mie UPDATEdichiarazioni sembrano strane. Esiste un modo migliore per aggiornare la [Account]riga corretta ?

Risposte:


13

1. Il trigger segue il principio ACID del database relazionale? È possibile che venga inserito un inserimento ma il trigger non riesce?

Questa domanda ha una risposta parziale a una domanda correlata a cui sei collegato. Il codice trigger viene eseguito nello stesso contesto transazionale dell'istruzione DML che lo ha causato, conservando la parte atomica dei principi ACID menzionati. L'istruzione di attivazione e il codice di attivazione hanno esito positivo o negativo come unità.

Le proprietà ACID garantiscono inoltre che l'intera transazione (incluso il codice trigger) lascerà il database in uno stato che non viola alcun vincolo esplicito ( coerente ) e che tutti gli effetti impegnati recuperabili sopravvivranno a un arresto anomalo del database ( durevole ).

A meno che la transazione circostante (forse implicita o con commit automatico) sia in esecuzione a SERIALIZABLElivello di isolamento , la proprietà Isolata non viene automaticamente garantita. Altre attività di database simultanee potrebbero interferire con il corretto funzionamento del codice trigger. Ad esempio, il saldo del conto potrebbe essere modificato da un'altra sessione dopo averlo letto e prima di aggiornarlo, una condizione di gara classica.

2. Le mie dichiarazioni IF e UPDATE sembrano strane. Esiste un modo migliore per aggiornare la riga [Account] corretta?

Vi sono ottime ragioni per cui l'altra domanda a cui hai collegato non offre soluzioni basate su trigger. Il codice di trigger progettato per mantenere sincronizzata una struttura denormalizzata può essere estremamente difficile da ottenere correttamente e testare correttamente. Anche persone molto avanzate di SQL Server con molti anni di esperienza lottano con questo.

Mantenere buone prestazioni contemporaneamente a preservare la correttezza in tutti gli scenari ed evitare problemi come deadlock aggiunge ulteriori dimensioni di difficoltà. Il tuo codice trigger non è affatto vicino per essere robusto e aggiorna il saldo di ogni account anche se viene modificata una sola transazione. Esistono tutti i tipi di rischi e sfide con una soluzione basata su trigger, che rende il compito profondamente inadatto per qualcuno relativamente nuovo in questo settore tecnologico.

Per illustrare alcuni dei problemi, mostro alcuni esempi di codice di seguito. Questa non è una soluzione rigorosamente testata (i trigger sono difficili!) E non sto suggerendo di usarla come qualcosa di diverso da un esercizio di apprendimento. Per un sistema reale, le soluzioni non trigger presentano importanti vantaggi, quindi è necessario rivedere attentamente le risposte all'altra domanda ed evitare completamente l'idea del trigger.

Tabelle di esempio

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

Prevenire TRUNCATE TABLE

I trigger non vengono attivati ​​da TRUNCATE TABLE. La seguente tabella vuota esiste esclusivamente per impedire il Transactionstroncamento della tabella (a cui fa riferimento una chiave esterna impedisce il troncamento della tabella):

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

Il seguente codice di attivazione garantisce che vengano mantenute solo le voci di account necessarie e utilizza SERIALIZABLElì la semantica. Come effetto collaterale auspicabile, ciò evita anche i risultati errati che potrebbero derivare se si utilizza un livello di isolamento di versione delle righe. Il codice evita inoltre di eseguire il codice trigger se l'istruzione di origine non ha interessato righe. La tabella temporanea e il RECOMPILEsuggerimento vengono utilizzati per evitare problemi del piano di esecuzione del trigger causati da stime imprecise della cardinalità:

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;

analisi

Il codice seguente utilizza una tabella di numeri per creare 100.000 account con saldo zero:

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

Il seguente codice di test inserisce 10.000 transazioni casuali:

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;

Utilizzando lo strumento SQLQueryStress , ho eseguito questo test 100 volte su 32 thread con buone prestazioni, senza deadlock e risultati corretti. Ancora non lo consiglio come qualcosa di diverso da un esercizio di apprendimento.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.