sql >> Databasteknik >  >> RDS >> Database

För sista gången, NEJ, du kan inte lita på IDENT_CURRENT()

Jag hade en diskussion igår med Kendal Van Dyke (@SQLDBA) om IDENT_CURRENT(). I grund och botten hade Kendal den här koden, som han hade testat och litat på på egen hand, och ville veta om han kunde lita på att IDENT_CURRENT() är korrekt i en högskalig, samtidig miljö:

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

Anledningen till att han var tvungen att göra detta är att han måste returnera det genererade IDENTITY-värdet till klienten. De typiska sätten vi gör detta på är:

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

Vissa av dessa är bättre än andra, men det har gjorts till döds, och jag tänker inte gå in på det här. I Kendals fall var IDENT_CURRENT hans sista och enda utväg, eftersom:

  • TableName hade en INSTEAD OF INSERT-utlösare, vilket gjorde både SCOPE_IDENTITY() och OUTPUT-satsen värdelösa från anroparen, eftersom:
    • SCOPE_IDENTITY() returnerar NULL, eftersom infogningen faktiskt skedde i ett annat omfång
    • OUTPUT-satsen genererar felmeddelande 334 på grund av triggern
  • Han eliminerade @@IDENTITY; Tänk på att utlösaren INSTEAD OF INSERT nu (eller senare kan ändras till) kan infogas i andra tabeller som har sina egna IDENTITY-kolumner, vilket skulle förstöra det returnerade värdet. Detta skulle också omintetgöra SCOPE_IDENTITY(), om det var möjligt.
  • Och slutligen kunde han inte använda OUTPUT-satsen (eller en resultatuppsättning från en andra fråga i den infogade pseudotabellen efter den eventuella infogningen) i utlösaren, eftersom denna funktion kräver en global inställning och har föråldrats sedan SQL Server 2005. Förståeligt nog måste Kendals kod vara framåtkompatibel och, när det är möjligt, inte helt förlita sig på vissa databas- eller serverinställningar.

Så, tillbaka till Kendals verklighet. Hans kod verkar säker nog – det är trots allt i en transaktion; vad kan gå fel? Tja, låt oss ta en titt på några viktiga meningar från IDENT_CURRENT-dokumentationen (betoning min, eftersom dessa varningar finns där av goda skäl):

Returnerar det senast genererade identitetsvärdet för en angiven tabell eller vy. Det senast genererade identitetsvärdet kan vara för alla sessioner och alla omfattning .

Var försiktig med att använda IDENT_CURRENT för att förutsäga nästa genererade identitetsvärde. det faktiska genererade värdet kan vara annorlunda från IDENT_CURRENT plus IDENT_INCR på grund av insättningar som utförts av andra sessioner .

Transaktioner nämns knappt i dokumentets brödtext (endast i samband med misslyckande, inte samtidighet), och inga transaktioner används i något av proverna. Så låt oss testa vad Kendal gjorde och se om vi kan få det att misslyckas när flera sessioner körs samtidigt. Jag ska skapa en loggtabell för att hålla reda på de värden som genereras av varje session – både identitetsvärdet som faktiskt genererades (med en efterutlösare) och värdet som påstås ha genererats enligt IDENT_CURRENT().

Först, tabellerna och 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

Öppna nu en handfull frågefönster och klistra in den här koden, kör dem så nära varandra som möjligt för att säkerställa största möjliga överlappning:

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 alla frågefönster har slutförts kör du den här frågan för att se några slumpmässiga rader där IDENT_CURRENT returnerade fel värde och en räkning av hur många rader totalt som påverkades av detta felrapporterade nummer:

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

Här är mina 10 rader för ett test:

Jag tyckte det var förvånande att nästan en tredjedel av raderna var avstängda. Dina resultat kommer säkert att variera och kan bero på hastigheten på dina enheter, återställningsmodell, loggfilinställningar eller andra faktorer. På två olika maskiner hade jag väldigt olika felfrekvens – med en faktor 10 (en långsammare maskin hade bara i närheten av 10 000 fel, eller ungefär 3%).

Det är direkt uppenbart att en transaktion inte räcker för att förhindra IDENT_CURRENT från att dra IDENTITY-värdena som genererats av andra sessioner. Vad sägs om en SERIALISERBAR transaktion? Rensa först de två tabellerna:

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

Lägg sedan till den här koden i början av skriptet i flera frågefönster och kör dem igen så samtidigt som möjligt:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Den här gången, när jag kör frågan mot IdentityLog-tabellen, visar den att SERIALIZABLE kan ha hjälpt lite, men det har inte löst problemet:

Och även om fel är fel, ser det ut från mina exempelresultat att IDENT_CURRENT-värdet vanligtvis bara är ett eller två av. Den här frågan bör dock ge att den kan vara *väg* off. I mina testkörningar var detta resultat så högt 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';

Genom dessa bevis kan vi dra slutsatsen att IDENT_CURRENT inte är transaktionssäkert. Det verkar påminna om ett liknande men nästan motsatt problem, där metadatafunktioner som OBJECT_NAME() blockeras – även när isoleringsnivån är LÄS OCMITTED – eftersom de inte följer omgivande isoleringssemantik. (Se Anslut artikel #432497 för mer information.)

På ytan, och utan att veta så mycket mer om arkitektur och tillämpning(ar), har jag inget riktigt bra förslag på Kendal; Jag vet bara att IDENT_CURRENT *inte* är svaret. :-) Använd det bara inte. För allt. Någonsin. När du läser värdet kan det redan vara fel.


  1. Hur man ansluter till värd PostgreSQL från vagrant virtualbox-maskin

  2. Använda Oracle JDeveloper 12c med Oracle Database 12c på Oracle Cloud Platform, del 3

  3. Hur exporterar och importerar jag en .sql-fil från kommandoraden med alternativ?

  4. Hur kan jag döda alla sessioner som ansluter till min Oracle-databas?