1. Le déclencheur suit-il le principe ACID de la base de données relationnelle? Y a-t-il une chance qu'une insertion puisse être validée mais le déclencheur échoue?
Cette question trouve une réponse partielle dans une question connexe à laquelle vous êtes lié. Le code de déclenchement est exécuté dans le même contexte transactionnel que l'instruction DML qui l'a provoqué, en préservant la partie atomique des principes ACID que vous mentionnez. L'instruction de déclenchement et le code de déclenchement réussissent ou échouent en tant qu'unité.
Les propriétés ACID garantissent également que la transaction entière (y compris le code de déclenchement) laissera la base de données dans un état qui ne viole aucune contrainte explicite ( cohérente ) et tout effet engagé récupérable survivra à un crash de la base de données ( durable ).
Sauf si la transaction environnante (peut-être implicite ou auto-commit) s'exécute au SERIALIZABLE
niveau d'isolement , la propriété Isolated n'est pas automatiquement garantie. D'autres activités de base de données simultanées peuvent interférer avec le bon fonctionnement de votre code de déclenchement. Par exemple, le solde du compte peut être modifié par une autre session après l'avoir lu et avant de le mettre à jour - une condition de concurrence classique.
2. Mes instructions IF et UPDATE semblent étranges. Existe-t-il un meilleur moyen de mettre à jour la ligne [Compte] correcte?
Il y a de très bonnes raisons pour lesquelles l'autre question à laquelle vous avez lié n'offre aucune solution basée sur les déclencheurs. Le code de déclenchement conçu pour maintenir une structure dénormalisée synchronisée peut être extrêmement difficile à obtenir correctement et à tester correctement. Même les personnes très avancées de SQL Server avec de nombreuses années d'expérience ont du mal avec cela.
Le maintien de bonnes performances tout en préservant l'exactitude dans tous les scénarios et en évitant les problèmes tels que les blocages ajoute des dimensions de difficulté supplémentaires. Votre code de déclenchement est loin d'être robuste et met à jour le solde de chaque compte même si une seule transaction est modifiée. Il existe toutes sortes de risques et de défis avec une solution basée sur les déclencheurs, ce qui rend la tâche profondément inappropriée pour une personne relativement nouvelle dans ce domaine technologique.
Pour illustrer certains des 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 vous suggère pas de l'utiliser comme autre chose qu'un exercice d'apprentissage. Pour un système réel, les solutions sans déclencheur 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éclencheur.
Exemples de tableaux
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 déclenchés par TRUNCATE TABLE
. La table vide suivante existe uniquement pour empêcher la Transactions
table d'être tronquée (le fait d'être référencé 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 du déclencheur
Le code de déclenchement suivant garantit que seules les entrées de compte nécessaires sont conservées et utilise la SERIALIZABLE
sémantique à cet endroit. En tant qu'effet secondaire souhaitable, cela évite également les résultats incorrects qui pourraient se produire si un niveau d'isolement de versionnage de ligne est utilisé. Le code évite également l'exécution du code de déclenchement si aucune ligne n'a été affectée par l'instruction source. La table temporaire et l' RECOMPILE
indicateur sont utilisés pour éviter les problèmes de plan d'exécution du déclencheur 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 une table de nombres pour créer 100 000 comptes avec un solde nul:
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 SQLQueryStress , j'ai exécuté ce test 100 fois sur 32 threads avec de bonnes performances, aucun blocage et des résultats corrects. Je ne recommande toujours pas cela comme autre chose qu'un exercice d'apprentissage.