sql >> Database >  >> RDS >> Database

Voor de laatste keer, NEE, u kunt IDENT_CURRENT() niet vertrouwen

Ik had gisteren een discussie met Kendal Van Dyke (@SQLDBA) over IDENT_CURRENT(). Kortom, Kendal had deze code, die hij zelf had getest en vertrouwde, en hij wilde weten of hij erop kon vertrouwen dat IDENT_CURRENT() nauwkeurig was in een grootschalige, gelijktijdige omgeving:

BEGIN TRANSACTION;
INSERT dbo.TableName(ColumnName) VALUES('Value');
SELECT IDENT_CURRENT('dbo.TableName');
COMMIT TRANSACTION;

De reden dat hij dit moest doen, is omdat hij de gegenereerde IDENTITY-waarde aan de klant moet retourneren. De typische manieren waarop we dit doen zijn:

  • SCOPE_IDENTITY()
  • OUTPUT-clausule
  • @@IDENTITY
  • IDENT_CURRENT()

Sommige hiervan zijn beter dan andere, maar dat is tot de dood toe gedaan, en ik ga er hier niet op ingaan. In het geval van Kendal was IDENT_CURRENT zijn laatste en enige redmiddel, omdat:

  • TableName had een INSTEAD OF INSERT-trigger, waardoor zowel SCOPE_IDENTITY() als de OUTPUT-clausule onbruikbaar waren voor de aanroeper, omdat:
    • SCOPE_IDENTITY() retourneert NULL, omdat het invoegen feitelijk in een ander bereik is gebeurd
    • de OUTPUT-clausule genereert fout Msg 334 vanwege de trigger
  • Hij elimineerde @@IDENTITY; bedenk dat de INSTEAD OF INSERT-trigger nu kan worden ingevoegd (of later kan worden gewijzigd in) in andere tabellen die hun eigen IDENTITY-kolommen hebben, wat de geretourneerde waarde zou verpesten. Dit zou ook SCOPE_IDENTITY() dwarsbomen, als het mogelijk was.
  • En tot slot, hij kon de OUTPUT-component (of een resultatenset van een tweede query van de ingevoegde pseudo-tabel na de uiteindelijke invoeging) niet gebruiken binnen de trigger, omdat deze mogelijkheid een globale instelling vereist en sindsdien is verouderd SQL Server 2005. Het is begrijpelijk dat de code van Kendal voorwaarts compatibel moet zijn en, indien mogelijk, niet volledig afhankelijk moet zijn van bepaalde database- of serverinstellingen.

Terug naar de realiteit van Kendal. Zijn code lijkt veilig genoeg - het zit tenslotte in een transactie; wat kan er fout gaan? Laten we eens kijken naar een paar belangrijke zinnen uit de IDENT_CURRENT-documentatie (nadruk van mij, want deze waarschuwingen zijn er niet voor niets):

Retourneert de laatste identiteitswaarde die is gegenereerd voor een opgegeven tabel of weergave. De laatst gegenereerde identiteitswaarde kan zijn voor elke sessie en elk bereik .

Wees voorzichtig met het gebruik van IDENT_CURRENT om de volgende gegenereerde identiteitswaarde te voorspellen. De werkelijk gegenereerde waarde kan afwijken van IDENT_CURRENT plus IDENT_INCR vanwege inserties uitgevoerd door andere sessies .

Transacties worden nauwelijks genoemd in de hoofdtekst van het document (alleen in de context van mislukking, niet gelijktijdigheid), en in geen van de voorbeelden worden transacties gebruikt. Laten we dus eens testen wat Kendal aan het doen was, en kijken of we het kunnen laten mislukken als er meerdere sessies tegelijk worden uitgevoerd. Ik ga een logtabel maken om de waarden bij te houden die door elke sessie zijn gegenereerd - zowel de identiteitswaarde die daadwerkelijk is gegenereerd (met behulp van een after-trigger), en de waarde waarvan wordt beweerd dat deze is gegenereerd volgens IDENT_CURRENT().

Eerst de tabellen en triggers:

-- 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

Open nu een handvol queryvensters, plak deze code en voer ze zo dicht mogelijk bij elkaar uit om de meeste overlap te garanderen:

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

Zodra alle queryvensters zijn voltooid, voert u deze query uit om een ​​paar willekeurige rijen te zien waarin IDENT_CURRENT de verkeerde waarde heeft geretourneerd, en een telling van hoeveel rijen in totaal zijn beïnvloed door dit verkeerd gerapporteerde aantal:

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();

Dit zijn mijn 10 rijen voor één test:

Ik vond het verrassend dat bijna een derde van de rijen eraf was. Uw resultaten zullen zeker variëren en kunnen afhankelijk zijn van de snelheid van uw schijven, herstelmodel, logbestandinstellingen of andere factoren. Op twee verschillende machines had ik enorm verschillende uitvalpercentages - met een factor 10 (een langzamere machine had slechts in de buurt van 10.000 fouten, of ongeveer 3%).

Het is meteen duidelijk dat een transactie niet voldoende is om te voorkomen dat IDENT_CURRENT de IDENTITY-waarden ophaalt die door andere sessies zijn gegenereerd. Wat dacht je van een SERIALISEERBARE transactie? Wis eerst de twee tabellen:

TRUNCATE TABLE dbo.TableName;
TRUNCATE TABLE dbo.IdentityLog;

Voeg vervolgens deze code toe aan het begin van het script in meerdere queryvensters en voer ze zo gelijktijdig mogelijk opnieuw uit:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Deze keer, toen ik de query uitvoer op de IdentityLog-tabel, blijkt dat SERIALIZABLE misschien een beetje heeft geholpen, maar het probleem is niet opgelost:

En hoewel fout fout is, blijkt uit mijn voorbeeldresultaten dat de IDENT_CURRENT-waarde meestal maar één of twee afwijkt. Deze query zou echter moeten opleveren dat deze *way* off kan zijn. In mijn testruns was dit resultaat zo hoog als 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';

Door dit bewijs kunnen we concluderen dat IDENT_CURRENT niet transactieveilig is. Het lijkt te doen denken aan een soortgelijk maar bijna tegengesteld probleem, waarbij metadatafuncties zoals OBJECT_NAME() worden geblokkeerd - zelfs wanneer het isolatieniveau LEZEN NIET-COMMITTED is - omdat ze de omringende isolatiesemantiek niet gehoorzamen. (Zie Connect Item #432497 voor meer details.)

Op het eerste gezicht, en zonder veel meer te weten over de architectuur en applicatie(s), heb ik niet echt een goede suggestie voor Kendal; Ik weet alleen dat IDENT_CURRENT *niet* het antwoord is. :-) Gebruik het gewoon niet. Voor alles. Ooit. Tegen de tijd dat u de waarde leest, kan deze al verkeerd zijn.


  1. Kan ik de tabelnaam in een voorbereide instructie parametriseren?

  2. Hoe een tafel laten vallen als deze bestaat?

  3. Lente 2011 PostgreSQL-conferenties, VS/Canada

  4. Hoe TRIM() werkt in MariaDB