Database
 sql >> Database >  >> RDS >> Database

Per l'ultima volta, NO, non puoi fidarti di IDENT_CURRENT()

Ho avuto una discussione ieri con Kendal Van Dyke (@SQLDBA) su IDENT_CURRENT(). Fondamentalmente, Kendal aveva questo codice, che aveva testato e di cui si fidava da solo, e voleva sapere se poteva fare affidamento sull'accuratezza di IDENT_CURRENT() in un ambiente simultaneo su larga scala:

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

Il motivo per cui ha dovuto farlo è perché ha bisogno di restituire il valore IDENTITY generato al client. I modi tipici in cui lo facciamo sono:

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

Alcuni di questi sono migliori di altri, ma è stato fatto a morte, e non ho intenzione di entrare nel merito qui. Nel caso di Kendal, IDENT_CURRENT era la sua ultima e unica risorsa, perché:

  • TableName aveva un trigger INSTEAD OF INSERT, rendendo inutili sia SCOPE_IDENTITY() che la clausola OUTPUT per il chiamante, perché:
    • SCOPE_IDENTITY() restituisce NULL, poiché l'inserimento è effettivamente avvenuto in un ambito diverso
    • la clausola OUTPUT genera un messaggio di errore 334 a causa del trigger
  • Ha eliminato @@IDENTITY; considera che il trigger INSTEAD OF INSERT potrebbe ora (o potrebbe essere modificato in seguito) in altre tabelle che hanno le proprie colonne IDENTITY, il che rovinerebbe il valore restituito. Ciò vanificherebbe anche SCOPE_IDENTITY(), se fosse possibile.
  • E infine, non poteva usare la clausola OUTPUT (o un insieme di risultati da una seconda query della pseudo-tabella inserita dopo l'eventuale inserimento) all'interno del trigger, perché questa funzionalità richiede un'impostazione globale ed è stata deprecata da allora SQL Server 2005. Comprensibilmente, il codice di Kendal deve essere compatibile con le versioni successive e, quando possibile, non basarsi completamente su determinati database o impostazioni del server.

Quindi, torniamo alla realtà di Kendal. Il suo codice sembra abbastanza sicuro – dopotutto è in una transazione; cosa potrebbe andare storto? Bene, diamo un'occhiata ad alcune frasi importanti dalla documentazione IDENT_CURRENT (sottolineatura mia, perché questi avvertimenti sono lì per una buona ragione):

Restituisce l'ultimo valore di identità generato per una tabella o una vista specificata. L'ultimo valore di identità generato può essere per qualsiasi sessione e qualsiasi ambito .

Prestare attenzione all'utilizzo di IDENT_CURRENT per prevedere il successivo valore di identità generato. Il valore effettivo generato potrebbe essere diverso da IDENT_CURRENT più IDENT_INCR a causa di inserimenti eseguiti da altre sessioni .

Le transazioni sono appena menzionate nel corpo del documento (solo nel contesto di un errore, non di concorrenza) e nessuna transazione viene utilizzata in nessuno dei campioni. Quindi, proviamo cosa stava facendo Kendal e vediamo se riusciamo a farlo fallire quando più sessioni sono in esecuzione contemporaneamente. Creerò una tabella di registro per tenere traccia dei valori generati da ogni sessione, sia il valore di identità che è stato effettivamente generato (usando un trigger successivo), sia il valore dichiarato di essere generato secondo IDENT_CURRENT().

Innanzitutto, le tabelle e i 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

Ora apri una manciata di finestre di query e incolla questo codice, eseguendole il più vicino possibile per garantire la massima sovrapposizione:

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

Una volta completate tutte le finestre di query, esegui questa query per visualizzare alcune righe casuali in cui IDENT_CURRENT ha restituito il valore errato e un conteggio di quante righe in totale sono state interessate da questo numero riportato in modo errato:

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

Ecco le mie 10 righe per un test:

Ho trovato sorprendente che quasi un terzo delle file fosse spento. I risultati varieranno sicuramente e potrebbero dipendere dalla velocità delle unità, dal modello di ripristino, dalle impostazioni del file di registro o da altri fattori. Su due macchine diverse, avevo percentuali di guasti molto diverse, di un fattore 10 (una macchina più lenta aveva solo circa 10.000 guasti, o circa il 3%).

Immediatamente è chiaro che una transazione non è sufficiente per impedire a IDENT_CURRENT di estrarre i valori IDENTITY generati da altre sessioni. Che ne dici di una transazione SERIALIZZABILE? Per prima cosa, cancella le due tabelle:

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

Quindi, aggiungi questo codice all'inizio dello script in più finestre di query ed eseguilo di nuovo il più contemporaneamente possibile:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Questa volta, quando eseguo la query sulla tabella IdentityLog, mostra che SERIALIZABLE potrebbe aver aiutato un po', ma non ha risolto il problema:

E mentre sbagliato è sbagliato, dai miei risultati di esempio sembra che il valore IDENT_CURRENT sia solitamente solo di uno o due. Tuttavia, questa query dovrebbe produrre che può essere *molto* disattivata. Nelle mie esecuzioni di test, questo risultato è stato di 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';

Attraverso queste prove possiamo concludere che IDENT_CURRENT non è sicuro per le transazioni. Sembra ricordare un problema simile ma quasi opposto, in cui le funzioni dei metadati come OBJECT_NAME() vengono bloccate, anche quando il livello di isolamento è READ UNCOMMITTED, perché non obbediscono alla semantica di isolamento circostante. (Vedi Connect Item #432497 per maggiori dettagli.)

In apparenza, e senza sapere molto di più sull'architettura e le applicazioni, non ho un buon suggerimento per Kendal; So solo che IDENT_CURRENT *non* è la risposta. :-) Basta non usarlo. Per qualsiasi cosa. Mai. Quando leggi il valore, potrebbe già essere sbagliato.