sql >> Databáze >  >> RDS >> Database

Naposledy, NE, nemůžete věřit IDENT_CURRENT()

Včera jsem diskutoval s Kendalem Van Dykem (@SQLDBA) o IDENT_CURRENT(). V podstatě měl Kendal tento kód, který sám otestoval a důvěřoval mu, a chtěl vědět, jestli se může spolehnout na to, že IDENT_CURRENT() je přesný ve velkém a souběžném prostředí:

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

Důvod, proč to musel udělat, je ten, že potřebuje vrátit vygenerovanou hodnotu IDENTITY klientovi. Typické způsoby, jak to děláme, jsou:

  • SCOPE_IDENTITY()
  • klauzule OUTPUT
  • @@IDENTITY
  • IDENT_CURRENT()

Některé z nich jsou lepší než jiné, ale to už je udělané k smrti a nebudu se tu do toho pouštět. V Kendalově případě byla IDENT_CURRENT jeho poslední a jedinou možností, protože:

  • TableName měl spouštěč MÍSTO INSERT, takže SCOPE_IDENTITY() i klauzule OUTPUT byly pro volajícího k ničemu, protože:
    • SCOPE_IDENTITY() vrátí hodnotu NULL, protože vložení ve skutečnosti proběhlo v jiném rozsahu
    • klauzule OUTPUT generuje chybovou zprávu 334 kvůli spouštěči
  • Odstranil @@IDENTITY; zvažte, že spouštěč INSTEAD OF INSERT by nyní mohl (nebo by mohl být později změněn) vkládat do jiných tabulek, které mají své vlastní sloupce IDENTITY, což by narušilo vrácenou hodnotu. To by také zmařilo SCOPE_IDENTITY(), pokud by to bylo možné.
  • A nakonec nemohl použít klauzuli OUTPUT (nebo sadu výsledků z druhého dotazu na vloženou pseudotabulku po případném vložení) v rámci spouštěče, protože tato schopnost vyžaduje globální nastavení a byla zastaralá od roku SQL Server 2005. Je pochopitelné, že Kendalův kód musí být dopředně kompatibilní a pokud je to možné, nespoléhat se zcela na určitá nastavení databáze nebo serveru.

Takže zpět do Kendalovy reality. Jeho kód se zdá být dostatečně bezpečný – koneckonců je součástí transakce; co by se mohlo pokazit? No, pojďme se podívat na pár důležitých vět z IDENT_CURRENT dokumentace (zdůrazněte můj, protože tato varování jsou tam z dobrého důvodu):

Vrátí poslední hodnotu identity vygenerovanou pro zadanou tabulku nebo pohled. Poslední vygenerovaná hodnota identity může být pro libovolnou relaci a jakýkoli rozsah .

Buďte opatrní při používání IDENT_CURRENT k predikci další generované hodnoty identity. skutečně vygenerovaná hodnota se může lišit od IDENT_CURRENT plus IDENT_INCR kvůli vkládání provedeným jinými relacemi .

V těle dokumentu nejsou transakce téměř zmíněny (pouze v kontextu selhání, nikoli souběžnosti) a v žádném ze vzorků nejsou použity žádné transakce. Pojďme tedy otestovat, co Kendal dělal, a uvidíme, jestli to dokážeme selhat, když běží více relací současně. Vytvořím tabulku protokolu pro sledování hodnot generovaných každou relací – jak hodnotu identity, která byla skutečně vygenerována (pomocí spouštěče po), tak hodnotu, o které se tvrdí, že byla vygenerována podle IDENT_CURRENT().

Nejprve tabulky a spouštěče:

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

Nyní otevřete několik oken dotazů a vložte tento kód a spusťte je co nejblíže k sobě, abyste zajistili co největší překrývání:

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

Jakmile budou dokončena všechna okna dotazu, spusťte tento dotaz, abyste viděli několik náhodných řádků, kde IDENT_CURRENT vrátil nesprávnou hodnotu, a počet řádků celkem ovlivněných tímto chybně nahlášeným číslem:

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

Zde je mých 10 řádků pro jeden test:

Překvapilo mě, že téměř třetina řad byla vypnutá. Vaše výsledky se budou jistě lišit a mohou záviset na rychlosti vašich disků, modelu obnovy, nastavení souboru protokolu nebo dalších faktorech. Na dvou různých strojích jsem měl značně rozdílnou poruchovost – faktorem 10 (pomalejší stroj měl jen kolem 10 000 poruch, tedy zhruba 3 %).

Okamžitě je jasné, že transakce nestačí k tomu, aby zabránila IDENT_CURRENT získat hodnoty IDENTITY generované jinými relacemi. Co takhle SERIALIZAČNÍ transakce? Nejprve vymažte dvě tabulky:

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

Poté přidejte tento kód na začátek skriptu ve více oknech dotazů a spusťte je znovu co možná souběžně:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Tentokrát, když spustím dotaz proti tabulce IdentityLog, ukáže se, že SERIALIZABLE možná trochu pomohla, ale nevyřešila problém:

A i když je špatně špatně, z mých ukázkových výsledků to vypadá, že hodnota IDENT_CURRENT je obvykle pouze o jednu nebo dvě. Tento dotaz by však měl vést k tomu, že může být *příliš* mimo. V mých testovacích jízdách byl tento výsledek 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';

Na základě těchto důkazů můžeme dojít k závěru, že IDENT_CURRENT není pro transakce bezpečné. Zdá se, že to připomíná podobný, ale téměř opačný problém, kdy jsou metadatové funkce jako OBJECT_NAME() zablokovány – i když je úroveň izolace READ UNCOMMITTED – protože nedodržují okolní sémantiku izolace. (Další podrobnosti viz Connect Item #432497.)

Na první pohled, a aniž bych věděl mnohem více o architektuře a aplikacích, nemám pro Kendala opravdu dobrý návrh; Vím jen, že IDENT_CURRENT *není* odpovědí. :-) Prostě to nepoužívejte. Na cokoli. Vůbec. V době, kdy si přečtete hodnotu, již může být chybná.


  1. Nesprávné řazení PostgreSQL

  2. Nainstalujte MariaDB na Mac

  3. Jak vybrat ze dvou tabulek v MySQL, i když ne všechny řádky v jedné tabulce mají korespondenty ve druhé?

  4. ERD notace v datovém modelování