web-dev-qa-db-fra.com

Synchronisation à l'aide de déclencheurs

J'ai une exigence similaire aux discussions précédentes à:

J'ai deux tables, [Account].[Balance] et [Transaction].[Amount]:

CREATE TABLE Account (
      AccountID    INT
    , Balance      MONEY
);

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

Quand il y a un insert, une mise à jour ou une suppression contre le [Transaction] table, le [Account].[Balance] devrait être mis à jour en fonction de la [Amount].

Actuellement, j'ai un déclencheur pour faire ce travail:

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

Bien que cela semble fonctionner, j'ai des questions:

  1. Le déclencheur suive-t-il le principe d'acide de la base de données relationnelle? Y a-t-il une chance qu'un insert pourrait être engagé, mais la gâchette échoue?
  2. Mes IF et UPDATE déclarations semblent étranges. Y a-t-il un meilleur moyen de mettre à jour le correct [Account] ligne?
11
Yiping

1. Le déclencheur suive-t-il le principe d'acide de la base de données relationnelle? Y a-t-il une chance qu'un insert pourrait être engagé, mais la gâchette échoue ?

Cette question est partiellement répondue dans une question connexe que vous avez liée à. Le code de déclenchement est exécuté dans le même contexte transactionnel que l'instruction DML qui le faisait tirer, préservant l'atomique partie des principes acides que vous mentionnez. La déclaration de déclenchement et le code déclencheur réussissent ou échouent comme une unité.

Les Propriétés acides garantissent également toute la transaction (y compris le code de déclenchement) laissera la base de données dans un état qui ne violera aucune contrainte explicite ( cohérente ) et les effets engagés recouvrables survivront à un crash de la base de données ( durable ).

Sauf si la transaction environnante (peut-être implicite ou auto-commiste) est en cours d'exécution au niveau SERIALIZABLE Niveau d'isolation , le isolé La propriété est non garantie automatiquement. Une autre activité de base de données simultanée pourrait interférer avec le bon fonctionnement de votre code de déclenchement. Par exemple, le solde du compte pourrait être modifié par une autre session après la lecture et avant de la mettre à jour - une condition de course classique.

2. Mes déclarations IF et Mettre à jour ont l'air étrange. Y a-t-il un meilleur moyen de mettre à jour la ligne [compte] correcte ?

Il y a de très bonnes raisons l'autre question que vous avez liée à n'offre aucune solution basée sur la gâchette. Code de déclenchement conçu pour conserver une structure dénormalisée synchronisée peut être extrêmement délicat pour obtenir correctement et tester correctement. Même des personnes SQL Server très avancées avec de nombreuses années d'expérience dans la lutte contre cela.

Maintenir de bonnes performances en même temps que la préservation de l'exactitude de tous les scénarios et éviter les problèmes tels que des blocages ajoute des dimensions supplémentaires de difficulté. Votre code de déclenchement est nulle part proche d'être robuste et met à jour la balance de chaque compte Même si seule une seule transaction est modifiée. Il existe toutes sortes de risques et de défis avec une solution basée sur la gâchette, ce qui rend la tâche profondément inappropriée pour une personne relativement nouvelle dans cette zone de technologie.

Pour illustrer certains problèmes, je montre un exemple de code ci-dessous. Ce n'est pas une solution rigoureusement testée (les déclencheurs sont difficiles!) Et je ne suggère pas que vous l'utilisez comme autre qu'un exercice d'apprentissage. Pour un système réel, les solutions de non-déclenchées présentent des avantages importants. Vous devez donc examiner attentivement les réponses à l'autre question et éviter complètement l'idée de déclenchement.

Tables d'échantillons

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

Prévenir TRUNCATE TABLE

Les déclencheurs ne sont pas tirés par TRUNCATE TABLE. La table vide suivante existe purement pour empêcher la table Transactions étant tronquée (être référencée par une clé étrangère empêche la troncature de la table):

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

Définition de déclenchement

Le code de déclenchement suivant garantit uniquement les entrées de compte nécessaires sont maintenues et utilise la sémantique SERIALIZABLE. En tant qu'école secondaire souhaitable, cela évite également les résultats incorrects qui pourraient résulter si un niveau d'isolement de la version à ligne est utilisé. Le code évite également d'exécuter le code de déclenchement si aucune ligne n'a été affectée par la déclaration source. La table temporaire et RECOMPILE indice sont utilisés pour éviter les problèmes de plan d'exécution de la gâchette causés par des estimations de cardinalité inexactes:

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;

Essai

Le code suivant utilise A Table des chiffres pour créer 100 000 comptes avec un équilibre zéro:

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

Le code de test ci-dessous insère 10 000 transactions aléatoires:

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;

À l'aide de l'outil SQLQuceryStress , j'ai couru ce test 100 fois sur 32 threads avec de bonnes performances, aucune impasse et des résultats corrects. Je ne recommande toujours pas cela comme autre chose qu'un exercice d'apprentissage.

13
Paul White 9