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:
IF
et UPDATE
déclarations semblent étranges. Y a-t-il un meilleur moyen de mettre à jour le correct [Account]
ligne?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.
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
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)
);
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;
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.