Jeg havde en diskussion i går med Kendal Van Dyke (@SQLDBA) om IDENT_CURRENT(). Grundlæggende havde Kendal denne kode, som han selv havde testet og stolet på, og ville vide, om han kunne stole på, at IDENT_CURRENT() var nøjagtig i et højskala, samtidig miljø:
BEGIN TRANSACTION; INSERT dbo.TableName(ColumnName) VALUES('Value'); SELECT IDENT_CURRENT('dbo.TableName'); COMMIT TRANSACTION;
Grunden til at han var nødt til at gøre dette, er fordi han skal returnere den genererede IDENTITY-værdi til klienten. De typiske måder, vi gør dette på, er:
- SCOPE_IDENTITY()
- OUTPUT-sætning
- @@IDENTITY
- IDENT_CURRENT()
Nogle af disse er bedre end andre, men det er gjort ihjel, og jeg vil ikke komme ind på det her. I Kendals tilfælde var IDENT_CURRENT hans sidste og eneste udvej, fordi:
- Tabelnavn havde en INSTEAD OF INSERT-trigger, hvilket gjorde både SCOPE_IDENTITY() og OUTPUT-sætningen ubrugelige fra kalderen, fordi:
- SCOPE_IDENTITY() returnerer NULL, da indsættelsen faktisk fandt sted i et andet omfang
- OUTPUT-sætningen genererer fejlmeddelelse 334 på grund af triggeren
- Han eliminerede @@IDENTITY; overveje, at INSTEAD OF INSERT-triggeren nu (eller senere kan blive ændret til) kan indsættes i andre tabeller, der har deres egne IDENTITY-kolonner, hvilket ville ødelægge den returnerede værdi. Dette ville også forpurre SCOPE_IDENTITY(), hvis det var muligt.
- Og endelig kunne han ikke bruge OUTPUT-sætningen (eller et resultatsæt fra en anden forespørgsel i den indsatte pseudo-tabel efter den endelige insert) i triggeren, fordi denne funktion kræver en global indstilling og er blevet forældet siden SQL Server 2005. Forståeligt nok skal Kendals kode være forward-kompatibel og, når det er muligt, ikke stole helt på bestemte database- eller serverindstillinger.
Så tilbage til Kendals virkelighed. Hans kode virker sikker nok – den er trods alt i en transaktion; hvad kan gå galt? Nå, lad os tage et kig på et par vigtige sætninger fra IDENT_CURRENT-dokumentationen (fremhæv min, fordi disse advarsler er der med god grund):
Returnerer den sidst genererede identitetsværdi for en specificeret tabel eller visning. Den sidst genererede identitetsværdi kan være for enhver session og ethvert omfang .…
Vær forsigtig med at bruge IDENT_CURRENT til at forudsige den næste genererede identitetsværdi. Den faktisk genererede værdi kan være anderledes fra IDENT_CURRENT plus IDENT_INCR på grund af indsættelser udført af andre sessioner .
Transaktioner er knap nævnt i selve dokumentet (kun i forbindelse med fejl, ikke samtidighed), og ingen transaktioner bruges i nogen af prøverne. Så lad os teste, hvad Kendal lavede, og se, om vi kan få det til at mislykkes, når flere sessioner kører samtidigt. Jeg vil oprette en logtabel for at holde styr på de værdier, der genereres af hver session – både den identitetsværdi, der faktisk blev genereret (ved hjælp af en efter-trigger), og den værdi, der hævdes at være genereret i henhold til IDENT_CURRENT().
Først tabellerne og triggerne:
-- 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
Åbn nu en håndfuld forespørgselsvinduer, og indsæt denne kode, og kør dem så tæt sammen som muligt for at sikre størst mulig overlapning:
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
Når alle forespørgselsvinduerne er afsluttet, skal du køre denne forespørgsel for at se et par tilfældige rækker, hvor IDENT_CURRENT returnerede den forkerte værdi, og en optælling af, hvor mange rækker i alt, der blev påvirket af dette forkert rapporterede antal:
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();
Her er mine 10 rækker til én test:
Jeg fandt det overraskende, at næsten en tredjedel af rækkerne var slukket. Dine resultater vil helt sikkert variere og kan afhænge af hastigheden på dine drev, gendannelsesmodel, logfilindstillinger eller andre faktorer. På to forskellige maskiner havde jeg vidt forskellige fejlfrekvenser – med en faktor 10 (en langsommere maskine havde kun omkring 10.000 fejl, eller omkring 3%).
Umiddelbart er det klart, at en transaktion ikke er nok til at forhindre IDENT_CURRENT i at trække IDENTITY-værdierne genereret af andre sessioner. Hvad med en SERIALISERbar transaktion? Ryd først de to tabeller:
TRUNCATE TABLE dbo.TableName; TRUNCATE TABLE dbo.IdentityLog;
Tilføj derefter denne kode til begyndelsen af scriptet i flere forespørgselsvinduer, og kør dem igen så samtidigt som muligt:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Denne gang, når jeg kører forespørgslen mod IdentityLog-tabellen, viser det, at SERIALIZABLE måske har hjulpet en lille smule, men det har ikke løst problemet:
Og selvom forkert er forkert, ser det ud fra mine prøveresultater, at IDENT_CURRENT-værdien normalt kun er slået en eller to. Denne forespørgsel skulle dog give, at den kan være *langt* off. I mine testkørsler var dette resultat så højt som 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';
Gennem dette bevis kan vi konkludere, at IDENT_CURRENT ikke er transaktionssikker. Det lader til at minde om et lignende, men næsten modsat problem, hvor metadatafunktioner som OBJECT_NAME() bliver blokeret – selv når isolationsniveauet er LÆST UNCOMMITTED – fordi de ikke adlyder omkringliggende isolationssemantik. (Se Connect Item #432497 for flere detaljer.)
På overfladen, og uden at vide meget mere om arkitekturen og anvendelsen(erne), har jeg ikke et rigtig godt forslag til Kendal; Jeg ved bare, at IDENT_CURRENT *ikke* er svaret. :-) Bare lad være med at bruge det. For alt. Nogensinde. Når du læser værdien, kan den allerede være forkert.