We wtorek w T-SQL w tym miesiącu Steve Jones (@way0utwest) poprosił nas o omówienie naszych najlepszych lub najgorszych doświadczeń z wyzwalaczami. Chociaż prawdą jest, że wyzwalacze są często niemile widziane, a nawet obawiają się, mają kilka ważnych przypadków użycia, w tym:
- Audyt (przed dodatkiem SP1 2016, kiedy ta funkcja stała się bezpłatna we wszystkich wydaniach)
- Egzekwowanie reguł biznesowych i integralności danych, gdy nie można ich łatwo zaimplementować w ograniczeniach, a nie chcesz, aby były zależne od kodu aplikacji lub samych zapytań DML
- Utrzymywanie historycznych wersji danych (przed przechwytywaniem danych zmian, śledzeniem zmian i tabelami czasowymi)
- Kolejkowanie alertów lub przetwarzanie asynchroniczne w odpowiedzi na określoną zmianę
- Zezwalanie na modyfikacje widoków (poprzez wyzwalacze ZAMIAST)
To nie jest wyczerpująca lista, tylko krótkie podsumowanie kilku scenariuszy, których doświadczyłem, w których wyzwalacze były wtedy właściwą odpowiedzią.
Kiedy wyzwalacze są konieczne, zawsze lubię badać użycie wyzwalaczy zamiast wyzwalaczy PO. Tak, są one trochę bardziej wstępne*, ale mają kilka całkiem ważnych zalet. Przynajmniej teoretycznie perspektywa zapobiegania działaniu (i jego konsekwencjom w dzienniku) wydaje się o wiele bardziej skuteczna niż pozwolenie na to, aby wszystko się wydarzyło, a następnie cofnięcie go.
*Mówię to, ponieważ musisz ponownie zakodować instrukcję DML w wyzwalaczu; dlatego nie nazywa się ich PRZED wyzwalaczami. Rozróżnienie jest tutaj ważne, ponieważ niektóre systemy implementują prawdziwe wyzwalacze PRZED, które po prostu uruchamiają się jako pierwsze. W SQL Server wyzwalacz INSTEAD OF skutecznie anuluje instrukcję, która spowodowała jego uruchomienie.
Załóżmy, że mamy prostą tabelę do przechowywania nazw kont. W tym przykładzie utworzymy dwie tabele, dzięki czemu będziemy mogli porównać dwa różne wyzwalacze i ich wpływ na czas trwania zapytania i wykorzystanie dziennika. Koncepcja polega na tym, że mamy regułę biznesową:nazwa konta nie jest obecna w innej tabeli, która reprezentuje „złe” nazwy, a wyzwalacz jest używany do wymuszenia tej reguły. Oto baza danych:
USE [master]; GO CREATE DATABASE [tr] ON (name = N'tr_dat', filename = N'C:\temp\tr.mdf', size = 4096MB) LOG ON (name = N'tr_log', filename = N'C:\temp\tr.ldf', size = 2048MB); GO ALTER DATABASE [tr] SET RECOVERY FULL; GO
A stoły:
USE [tr]; GO CREATE TABLE dbo.Accounts_After ( AccountID int PRIMARY KEY, name sysname UNIQUE, filler char(255) NOT NULL DEFAULT '' ); CREATE TABLE dbo.Accounts_Instead ( AccountID int PRIMARY KEY, name sysname UNIQUE, filler char(255) NOT NULL DEFAULT '' ); CREATE TABLE dbo.InvalidNames ( name sysname PRIMARY KEY ); INSERT dbo.InvalidNames(name) VALUES (N'poop'),(N'hitler'),(N'boobies'),(N'cocaine');
I wreszcie wyzwalacze. Dla uproszczenia mamy do czynienia tylko z wstawkami, i zarówno w przypadku po, jak i zamiast, po prostu przerwiemy całą partię, jeśli jakakolwiek nazwa narusza naszą regułę:
CREATE TRIGGER dbo.tr_Accounts_After ON dbo.Accounts_After AFTER INSERT AS BEGIN IF EXISTS ( SELECT 1 FROM inserted AS i INNER JOIN dbo.InvalidNames AS n ON i.name = n.name ) BEGIN RAISERROR(N'Tsk tsk.', 11, 1); ROLLBACK TRANSACTION; RETURN; END END GO CREATE TRIGGER dbo.tr_Accounts_Instead ON dbo.Accounts_After INSTEAD OF INSERT AS BEGIN IF EXISTS ( SELECT 1 FROM inserted AS i INNER JOIN dbo.InvalidNames AS n ON i.name = n.name ) BEGIN RAISERROR(N'Tsk tsk.', 11, 1); RETURN; END ELSE BEGIN INSERT dbo.Accounts_Instead(AccountID, name, filler) SELECT AccountID, name, filler FROM inserted; END END GO
Teraz, aby przetestować wydajność, spróbujemy po prostu wstawić 100 000 nazw do każdej tabeli, z przewidywalnym wskaźnikiem niepowodzeń wynoszącym 10%. Innymi słowy, 90 000 nazw jest w porządku, pozostałe 10 000 nie przechodzi testu i powoduje, że wyzwalacz albo cofa się, albo nie jest wstawiany, w zależności od partii.
Najpierw musimy zrobić trochę porządków przed każdą partią:
TRUNCATE TABLE dbo.Accounts_Instead; TRUNCATE TABLE dbo.Accounts_After; GO CHECKPOINT; CHECKPOINT; BACKUP LOG triggers TO DISK = N'C:\temp\tr.trn' WITH INIT, COMPRESSION; GO
Zanim zaczniemy mięso z każdej partii, policzymy wiersze w dzienniku transakcji i zmierzymy rozmiar oraz wolne miejsce. Następnie przejdziemy przez kursor, aby przetworzyć 100 000 wierszy w losowej kolejności, próbując wstawić każdą nazwę do odpowiedniej tabeli. Kiedy skończymy, ponownie zmierzymy liczbę wierszy i rozmiar dziennika oraz sprawdzimy czas trwania.
SET NOCOUNT ON; DECLARE @batch varchar(10) = 'After', -- or After @d datetime2(7) = SYSUTCDATETIME(), @n nvarchar(129), @i int, @err nvarchar(512); -- measure before and again when we're done: SELECT COUNT(*) FROM sys.fn_dblog(NULL, NULL); SELECT CurrentSizeMB = size/128.0, FreeSpaceMB = (size-CONVERT(int, FILEPROPERTY(name,N'SpaceUsed')))/128.0 FROM sys.database_files WHERE name = N'tr_log'; DECLARE c CURSOR LOCAL FAST_FORWARD FOR SELECT name, i = ROW_NUMBER() OVER (ORDER BY NEWID()) FROM ( SELECT DISTINCT TOP (90000) LEFT(o.name,64) + '/' + LEFT(c.name,63) FROM sys.all_objects AS o CROSS JOIN sys.all_columns AS c UNION ALL SELECT TOP (10000) N'boobies' FROM sys.all_columns ) AS x (name) ORDER BY i; OPEN c; FETCH NEXT FROM c INTO @n, @i; WHILE @@FETCH_STATUS = 0 BEGIN BEGIN TRY IF @batch = 'After' INSERT dbo.Accounts_After(AccountID,name) VALUES(@i,@n); IF @batch = 'Instead' INSERT dbo.Accounts_Instead(AccountID,name) VALUES(@i,@n); END TRY BEGIN CATCH SET @err = ERROR_MESSAGE(); END CATCH FETCH NEXT FROM c INTO @n, @i; END -- measure again when we're done: SELECT COUNT(*) FROM sys.fn_dblog(NULL, NULL); SELECT duration = DATEDIFF(MILLISECOND, @d, SYSUTCDATETIME()), CurrentSizeMB = size/128.0, FreeSpaceMB = (size-CAST(FILEPROPERTY(name,N'SpaceUsed') AS int))/128.0 FROM sys.database_files WHERE name = N'tr_log'; CLOSE c; DEALLOCATE c;
Wyniki (uśrednione z 5 przebiegów każdej partii):
PO vs. ZAMIAST:Wyniki
W moich testach użycie dziennika było prawie identyczne, z ponad 10% większą liczbą wierszy dziennika generowanych przez wyzwalacz INSTEAD OF. Pod koniec każdej partii kopałem trochę:
SELECT [Operation], COUNT(*) FROM sys.fn_dblog(NULL, NULL) GROUP BY [Operation] ORDER BY [Operation];
A oto typowy wynik (podkreśliłem główne delty):
Dystrybucja wierszy dziennika
Zagłębię się w to głębiej innym razem.
Ale kiedy się do tego zabrać…
…najważniejszą metryką prawie zawsze będzie czas trwania , aw moim przypadku spust INSTEAD OF działał co najmniej 5 sekund szybciej w każdym pojedynczym teście head-to-head. Jeśli to wszystko brzmi znajomo, tak, rozmawiałem o tym wcześniej, ale wtedy nie zauważyłem tych samych objawów w wierszach dziennika.
Pamiętaj, że może to nie być dokładny schemat lub obciążenie, możesz mieć bardzo inny sprzęt, współbieżność może być wyższa, a wskaźnik niepowodzeń może być znacznie wyższy (lub niższy). Moje testy przeprowadzono na izolowanej maszynie z dużą ilością pamięci i bardzo szybkimi dyskami SSD PCIe. Jeśli dziennik znajduje się na wolniejszym dysku, różnice w wykorzystaniu dziennika mogą przeważyć nad innymi metrykami i znacznie zmienić czas trwania. Wszystkie te czynniki (i więcej!) mogą wpłynąć na Twoje wyniki, więc powinieneś testować w swoim środowisku.
Chodzi jednak o to, że wyzwalacze zamiast wyzwalaczy mogą być lepiej dopasowane. Teraz, gdybyśmy tylko mogli uzyskać wyzwalacze ZAMIAST DDL…