1. Czy wyzwalacz jest zgodny z zasadą ACID relacyjnej bazy danych? Czy istnieje szansa, że wstawka może zostać zatwierdzona, ale wyzwalacz nie powiedzie się?
Odpowiedź na to pytanie jest częściowo udzielona w powiązanym z nim pytaniu . Kod wyzwalający jest wykonywany w tym samym kontekście transakcyjnym, co instrukcja DML, która spowodowała jego uruchomienie, zachowując część atomową zasad ACID, o których wspomniałeś. Instrukcja wyzwalająca i kod wyzwalający zarówno działają jak i kończą się niepowodzeniem jako jednostka.
Właściwości ACID gwarantują również, że cała transakcja (łącznie z kodem wyzwalacza) opuści bazę danych w stanie, który nie narusza żadnych wyraźnych ograniczeń ( spójne ), a wszelkie przywracane zatwierdzone efekty przetrwają awarię bazy danych ( trwałą ).
O ile otaczająca transakcja (być może niejawna lub autoryzacja) nie działa na SERIALIZABLE
poziomie izolacji , właściwość Izolacja nie jest automatycznie gwarantowana. Inna jednoczesna aktywność bazy danych może zakłócać prawidłowe działanie kodu wyzwalacza. Na przykład saldo konta może zostać zmienione przez inną sesję po jego przeczytaniu i przed aktualizacją - klasyczny warunek wyścigu.
2. Moje instrukcje IF i UPDATE wyglądają dziwnie. Czy istnieje lepszy sposób zaktualizowania prawidłowego wiersza [Konto]?
Istnieją bardzo dobre powody, dla których drugie pytanie, do którego się odnosisz, nie oferuje żadnych rozwiązań opartych na wyzwalaczach. Kod wyzwalający zaprojektowany do synchronizacji zdenormalizowanej struktury może być bardzo trudny do poprawnego wykonania i prawidłowego przetestowania. Walczą z tym nawet bardzo zaawansowani ludzie programu SQL Server z wieloletnim doświadczeniem.
Utrzymanie dobrej wydajności przy jednoczesnym zachowaniu poprawności we wszystkich scenariuszach i unikanie problemów takich jak zakleszczenia dodaje dodatkowych wymiarów trudności. Twój kod aktywacyjny nie jest zbyt blisko, aby być niezawodnym i aktualizuje saldo każdego konta, nawet jeśli zmodyfikowana jest tylko jedna transakcja. Istnieje wiele rodzajów ryzyka i wyzwań związanych z rozwiązaniem opartym na wyzwalaczu, co sprawia, że zadanie jest całkowicie nieodpowiednie dla kogoś stosunkowo nowego w tej dziedzinie technologii.
Aby zilustrować niektóre problemy, poniżej pokazuję przykładowy kod. To nie jest dokładnie przetestowane rozwiązanie (wyzwalacze są trudne!) I nie sugeruję, abyś używał go jako czegoś innego niż ćwiczenie edukacyjne. W prawdziwym systemie rozwiązania nie wyzwalające mają ważne zalety, dlatego należy dokładnie przejrzeć odpowiedzi na inne pytanie i całkowicie unikać pomysłu na wyzwalacz.
Przykładowe tabele
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)
);
Zapobieganie TRUNCATE TABLE
Wyzwalacze nie są uruchamiane przez TRUNCATE TABLE
. Poniższa pusta tabela istnieje wyłącznie po to, aby zapobiec Transactions
obcinaniu tabeli (odwoływanie się do niej przez klucz obcy zapobiega obcinaniu tabeli):
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)
);
Definicja wyzwalacza
Poniższy kod wyzwalacza zapewnia utrzymanie tylko niezbędnych wpisów konta i używa SERIALIZABLE
tam semantyki. Jako pożądany efekt uboczny pozwala to również uniknąć niepoprawnych wyników, które mogą wystąpić, jeśli używany jest poziom izolacji wersji wierszowej. Kod unika także wykonywania kodu wyzwalającego, jeśli instrukcja source nie ma wpływu na wiersze. Tabela tymczasowa i RECOMPILE
podpowiedź służą do uniknięcia problemów z planem wykonania wyzwalacza spowodowanych niedokładnymi szacunkami liczności:
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;
Testowanie
Poniższy kod wykorzystuje tabelę liczb do utworzenia 100 000 kont z zerowym saldem:
INSERT dbo.Accounts
(AccountID, Balance)
SELECT
N.n, $0
FROM dbo.Numbers AS N
WHERE
N.n BETWEEN 1 AND 100000;
Poniższy kod testowy wstawia 10 000 losowych transakcji:
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;
Za pomocą narzędzia SQLQueryStress uruchomiłem ten test 100 razy w 32 wątkach z dobrą wydajnością, bez zakleszczeń i poprawnymi wynikami. Nadal nie polecam tego jako czegoś innego niż ćwiczenie edukacyjne.