Ich hatte gestern eine Diskussion mit Kendal Van Dyke (@SQLDBA) über IDENT_CURRENT(). Im Grunde hatte Kendal diesen Code, den er selbst getestet und dem er vertraut hatte, und wollte wissen, ob er sich darauf verlassen konnte, dass IDENT_CURRENT() in einer hochskalierten, gleichzeitigen Umgebung korrekt ist:
BEGIN TRANSACTION; INSERT dbo.TableName(ColumnName) VALUES('Value'); SELECT IDENT_CURRENT('dbo.TableName'); COMMIT TRANSACTION;
Der Grund dafür ist, dass er den generierten IDENTITY-Wert an den Client zurückgeben muss. Die typischen Wege, wie wir dies tun, sind:
- SCOPE_IDENTITY()
- OUTPUT-Klausel
- @@IDENTITÄT
- IDENT_CURRENT()
Einige davon sind besser als andere, aber das wurde zu Tode getan, und ich werde hier nicht darauf eingehen. In Kendals Fall war IDENT_CURRENT sein letzter und einziger Ausweg, weil:
- TableName hatte einen INSTEAD OF INSERT-Trigger, wodurch sowohl SCOPE_IDENTITY() als auch die OUTPUT-Klausel für den Aufrufer nutzlos wurden, weil:
- SCOPE_IDENTITY() gibt NULL zurück, da die Einfügung tatsächlich in einem anderen Geltungsbereich stattfand
- Die OUTPUT-Klausel generiert wegen des Triggers die Fehlermeldung 334
- Er eliminierte @@IDENTITY; Bedenken Sie, dass der INSTEAD OF INSERT-Trigger jetzt in andere Tabellen einfügen könnte (oder später geändert werden könnte), die ihre eigenen IDENTITY-Spalten haben, was den zurückgegebenen Wert durcheinander bringen würde. Dies würde auch SCOPE_IDENTITY() vereiteln, wenn es möglich wäre.
- Und schließlich konnte er die OUTPUT-Klausel (oder eine Ergebnismenge aus einer zweiten Abfrage der eingefügten Pseudotabelle nach der eventuellen Einfügung) nicht innerhalb des Triggers verwenden, da diese Fähigkeit eine globale Einstellung erfordert und seitdem veraltet ist SQL Server 2005. Verständlicherweise muss der Code von Kendal aufwärtskompatibel sein und sich möglichst nicht vollständig auf bestimmte Datenbank- oder Servereinstellungen verlassen.
Also zurück zu Kendals Realität. Sein Code scheint sicher genug zu sein – schließlich befindet er sich in einer Transaktion; was könnte schiefgehen? Schauen wir uns ein paar wichtige Sätze aus der IDENT_CURRENT-Dokumentation an (Hervorhebung von mir, weil diese Warnungen aus gutem Grund vorhanden sind):
Gibt den letzten Identitätswert zurück, der für eine angegebene Tabelle oder Ansicht generiert wurde. Der zuletzt generierte Identitätswert kann für jede Sitzung gelten und beliebiger Bereich .…
Seien Sie vorsichtig bei der Verwendung von IDENT_CURRENT, um den nächsten generierten Identitätswert vorherzusagen. Der tatsächlich generierte Wert kann davon abweichen von IDENT_CURRENT plus IDENT_INCR aufgrund von Einfügungen, die von anderen Sitzungen durchgeführt wurden .
Transaktionen werden im Hauptteil des Dokuments kaum erwähnt (nur im Zusammenhang mit Fehlern, nicht Parallelität), und in keinem der Beispiele werden Transaktionen verwendet. Lassen Sie uns also testen, was Kendal getan hat, und sehen, ob wir es zum Scheitern bringen können, wenn mehrere Sitzungen gleichzeitig ausgeführt werden. Ich werde eine Protokolltabelle erstellen, um die von jeder Sitzung generierten Werte zu verfolgen – sowohl den Identitätswert, der tatsächlich generiert wurde (unter Verwendung eines After-Triggers), als auch den Wert, der laut IDENT_CURRENT() angeblich generiert wurde.
Zuerst die Tabellen und Trigger:
-- 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
Öffnen Sie nun eine Handvoll Abfragefenster und fügen Sie diesen Code ein, wobei Sie sie so dicht wie möglich ausführen, um die größtmögliche Überlappung sicherzustellen:
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
Nachdem alle Abfragefenster abgeschlossen sind, führen Sie diese Abfrage aus, um einige zufällige Zeilen anzuzeigen, in denen IDENT_CURRENT den falschen Wert zurückgegeben hat, und um zu ermitteln, wie viele Zeilen insgesamt von dieser falsch gemeldeten Zahl betroffen waren:
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();
Hier sind meine 10 Zeilen für einen Test:
Ich fand es überraschend, dass fast ein Drittel der Reihen aus waren. Ihre Ergebnisse werden sicherlich variieren und können von der Geschwindigkeit Ihrer Laufwerke, dem Wiederherstellungsmodell, den Protokolldateieinstellungen oder anderen Faktoren abhängen. Auf zwei verschiedenen Computern hatte ich sehr unterschiedliche Ausfallraten – um den Faktor 10 (ein langsamerer Computer hatte nur etwa 10.000 Ausfälle oder ungefähr 3 %).
Es ist sofort klar, dass eine Transaktion nicht ausreicht, um IDENT_CURRENT daran zu hindern, die von anderen Sitzungen generierten IDENTITY-Werte abzurufen. Wie wäre es mit einer SERIALIZABLE-Transaktion? Leeren Sie zuerst die beiden Tabellen:
TRUNCATE TABLE dbo.TableName; TRUNCATE TABLE dbo.IdentityLog;
Fügen Sie dann diesen Code am Anfang des Skripts in mehreren Abfragefenstern hinzu und führen Sie sie so gleichzeitig wie möglich erneut aus:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Wenn ich diesmal die Abfrage für die IdentityLog-Tabelle ausführe, zeigt sich, dass SERIALIZABLE vielleicht ein wenig geholfen hat, aber das Problem nicht gelöst hat:
Und obwohl falsch falsch ist, sieht es aus meinen Beispielergebnissen so aus, dass der IDENT_CURRENT-Wert normalerweise nur um ein oder zwei abweicht. Diese Abfrage sollte jedoch ergeben, dass es *weit* daneben liegen kann. In meinen Testläufen lag dieses Ergebnis bei bis zu 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';
Aus diesem Beweis können wir schließen, dass IDENT_CURRENT nicht transaktionssicher ist. Es scheint an ein ähnliches, aber fast entgegengesetztes Problem zu erinnern, bei dem Metadatenfunktionen wie OBJECT_NAME() blockiert werden – selbst wenn die Isolationsstufe READ UNCOMMITTED ist – weil sie der umgebenden Isolationssemantik nicht gehorchen. (Weitere Einzelheiten finden Sie unter Connect Item #432497.)
An der Oberfläche und ohne viel mehr über die Architektur und Anwendung(en) zu wissen, habe ich keinen wirklich guten Vorschlag für Kendal; Ich weiß nur, dass IDENT_CURRENT *nicht* die Antwort ist. :-) Nur nicht verwenden. Für alles. Je. Wenn Sie den Wert ablesen, kann er bereits falsch sein.