Wczoraj rozmawiałem z Kendal Van Dyke (@SQLDBA) na temat IDENT_CURRENT(). Zasadniczo Kendal miał ten kod, który sam przetestował i któremu zaufał, i chciał wiedzieć, czy może polegać na dokładności IDENT_CURRENT() w środowisku współbieżnym na dużą skalę:
BEGIN TRANSACTION; INSERT dbo.TableName(ColumnName) VALUES('Value'); SELECT IDENT_CURRENT('dbo.TableName'); COMMIT TRANSACTION;
Powodem, dla którego musiał to zrobić, jest to, że musi zwrócić klientowi wygenerowaną wartość IDENTITY. Typowe sposoby, w jakie to robimy, to:
- SCOPE_IDENTITY()
- klauzula OUTPUT
- @@TOŻSAMOŚĆ
- IDENT_CURRENT()
Niektóre z nich są lepsze od innych, ale zostało to zrobione na śmierć i nie zamierzam się tutaj w to wnikać. W przypadku Kendala IDENT_CURRENT było jego ostatnią i jedyną deską ratunku, ponieważ:
- TableName miał wyzwalacz INSTEAD OF INSERT, przez co zarówno SCOPE_IDENTITY(), jak i klauzula OUTPUT są bezużyteczne dla wywołującego, ponieważ:
- SCOPE_IDENTITY() zwraca NULL, ponieważ wstawienie faktycznie miało miejsce w innym zakresie
- klauzula OUTPUT generuje błąd Msg 334 z powodu wyzwalacza
- Wyeliminował @@IDENTITY; należy wziąć pod uwagę, że wyzwalacz INSTEAD OF INSERT może teraz (lub później zostać zmieniony na) wstawić do innych tabel, które mają własne kolumny IDENTITY, co może zepsuć zwracaną wartość. To również udaremniłoby SCOPE_IDENTITY(), gdyby było to możliwe.
- I na koniec nie mógł użyć klauzuli OUTPUT (lub wyniku z drugiego zapytania wstawionej pseudotabeli po ewentualnym wstawieniu) w wyzwalaczu, ponieważ ta właściwość wymaga ustawienia globalnego i jest przestarzała od SQL Server 2005. Zrozumiałe jest, że kod Kendal musi być zgodny w przód i, jeśli to możliwe, nie polegać całkowicie na pewnych ustawieniach bazy danych lub serwera.
Wróćmy więc do rzeczywistości Kendala. Jego kod wydaje się wystarczająco bezpieczny – w końcu jest w transakcji; Co mogłoby pójść źle? Cóż, spójrzmy na kilka ważnych zdań z dokumentacji IDENT_CURRENT (podkreślenie moje, bo te ostrzeżenia są tam nie bez powodu):
Zwraca ostatnią wartość tożsamości wygenerowaną dla określonej tabeli lub widoku. Ostatnia wygenerowana wartość tożsamości może dotyczyć dowolnej sesji i dowolny zakres .…
Zachowaj ostrożność podczas używania IDENT_CURRENT do przewidywania następnej wygenerowanej wartości tożsamości. faktycznie wygenerowana wartość może być inna z IDENT_CURRENT plus IDENT_INCR ze względu na wstawienia wykonane przez inne sesje .
Transakcje są ledwie wymienione w treści dokumentu (tylko w kontekście niepowodzenia, a nie współbieżności), aw żadnej z próbek nie są używane żadne transakcje. Przetestujmy więc, co robił Kendal, i zobaczmy, czy możemy sprawić, by nie zadziałał, gdy wiele sesji działa jednocześnie. Zamierzam utworzyć tabelę dziennika, aby śledzić wartości generowane przez każdą sesję – zarówno wartość tożsamości, która została faktycznie wygenerowana (przy użyciu wyzwalacza po), jak i wartość, która ma zostać wygenerowana zgodnie z IDENT_CURRENT().
Po pierwsze, tabele i wyzwalacze:
-- the destination table: CREATE TABLE dbo.TableName ( ID INT IDENTITY(1,1), seq INT ); -- the log table: CREATE TABLE dbo.IdentityLog ( SPID INT, seq INT, src VARCHAR(20), -- trigger or ident_current id INT ); GO -- the trigger, adding my logging: CREATE TRIGGER dbo.InsteadOf_TableName ON dbo.TableName INSTEAD OF INSERT AS BEGIN INSERT dbo.TableName(seq) SELECT seq FROM inserted; -- this is just for our logging purposes here: INSERT dbo.IdentityLog(SPID,seq,src,id) SELECT @@SPID, seq, 'trigger', SCOPE_IDENTITY() FROM inserted; END GO
Teraz otwórz kilka okien zapytań i wklej ten kod, wykonując je jak najbliżej siebie, aby zapewnić jak największe nakładanie się:
SET NOCOUNT ON; DECLARE @seq INT = 0; WHILE @seq <= 100000 BEGIN BEGIN TRANSACTION; INSERT dbo.TableName(seq) SELECT @seq; INSERT dbo.IdentityLog(SPID,seq,src,id) SELECT @@SPID,@seq,'ident_current',IDENT_CURRENT('dbo.TableName'); COMMIT TRANSACTION; SET @seq += 1; END
Po zakończeniu wszystkich okien zapytań uruchom to zapytanie, aby zobaczyć kilka losowych wierszy, w których identyfikator IDENT_CURRENT zwrócił nieprawidłową wartość, oraz liczbę wierszy, na które w sumie wpłynęła ta błędnie zgłoszona liczba:
SELECT TOP (10) id_cur.SPID, [ident_current] = id_cur.id, [actual id] = tr.id, total_bad_results = COUNT(*) OVER() FROM dbo.IdentityLog AS id_cur INNER JOIN dbo.IdentityLog AS tr ON id_cur.SPID = tr.SPID AND id_cur.seq = tr.seq AND id_cur.id <> tr.id WHERE id_cur.src = 'ident_current' AND tr.src = 'trigger' ORDER BY NEWID();
Oto moje 10 wierszy dla jednego testu:
Zaskoczyło mnie, że prawie jedna trzecia rzędów była wyłączona. Twoje wyniki z pewnością będą się różnić i mogą zależeć od szybkości dysków, modelu odzyskiwania, ustawień pliku dziennika lub innych czynników. Na dwóch różnych maszynach miałem bardzo różne współczynniki awarii – dziesięciokrotnie (wolniejsza maszyna miała tylko około 10 000 awarii, czyli około 3%).
Natychmiast staje się jasne, że transakcja nie wystarczy, aby uniemożliwić IDENT_CURRENT pobranie wartości IDENTITY generowanych przez inne sesje. Co powiesz na transakcję SERIALIZOWALNĄ? Najpierw wyczyść dwie tabele:
TRUNCATE TABLE dbo.TableName; TRUNCATE TABLE dbo.IdentityLog;
Następnie dodaj ten kod na początku skryptu w wielu oknach zapytań i uruchom je ponownie tak współbieżnie, jak to możliwe:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Tym razem, kiedy uruchamiam zapytanie w tabeli IdentityLog, pokazuje, że SERIALIZABLE może trochę pomogło, ale nie rozwiązało problemu:
I chociaż błąd jest zły, z moich przykładowych wyników wynika, że wartość IDENT_CURRENT jest zwykle przesunięta tylko o jeden lub dwa. Jednak to zapytanie powinno dać wynik, że może być *way* off. W moich testach wynik ten wyniósł aż 236:
SELECT MAX(ABS(id_cur.id - tr.id)) FROM dbo.IdentityLog AS id_cur INNER JOIN dbo.IdentityLog AS tr ON id_cur.SPID = tr.SPID AND id_cur.seq = tr.seq AND id_cur.id <> tr.id WHERE id_cur.src = 'ident_current' AND tr.src = 'trigger';
Dzięki tym dowodom możemy stwierdzić, że IDENT_CURRENT nie jest bezpieczny dla transakcji. Wydaje się, że przypomina to podobny, ale prawie odwrotny problem, w którym funkcje metadanych, takie jak OBJECT_NAME(), są blokowane – nawet jeśli poziom izolacji jest READ UNCOMMITTED – ponieważ nie przestrzegają otaczającej semantyki izolacji. (Zobacz artykuł Connect #432497, aby uzyskać więcej informacji).
Na pozór i nie wiedząc dużo więcej o architekturze i aplikacjach, nie mam naprawdę dobrej sugestii dla Kendal; Po prostu wiem, że IDENT_CURRENT *nie* jest odpowiedzią. :-) Po prostu tego nie używaj. Na wszystko. Kiedykolwiek. Zanim odczytasz wartość, może już być błędna.