sql >> Base de Datos >  >> RDS >> Database

Por última vez, NO, no puedes confiar en IDENT_CURRENT()

Ayer tuve una conversación con Kendal Van Dyke (@SQLDBA) sobre IDENT_CURRENT(). Básicamente, Kendal tenía este código, que había probado y en el que confiaba, y quería saber si podía confiar en que IDENT_CURRENT() fuera preciso en un entorno simultáneo a gran escala:

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

La razón por la que tuvo que hacer esto es porque necesita devolver el valor de IDENTIDAD generado al cliente. Las formas típicas en que hacemos esto son:

  • SCOPE_IDENTITY()
  • cláusula OUTPUT
  • @@IDENTIDAD
  • IDENT_ACTUAL()

Algunos de estos son mejores que otros, pero eso se ha hecho hasta la saciedad, y no voy a entrar en eso aquí. En el caso de Kendal, IDENT_CURRENT fue su último y único recurso porque:

  • TableName tenía un disparador INSTEAD OF INSERT, lo que hacía que tanto SCOPE_IDENTITY() como la cláusula OUTPUT fueran inútiles para la persona que llama, porque:
    • SCOPE_IDENTITY() devuelve NULL, ya que la inserción en realidad ocurrió en un ámbito diferente
    • la cláusula OUTPUT genera el mensaje de error 334 debido al activador
  • Eliminó a @@IDENTITY; considere que el disparador INSTEAD OF INSERT ahora podría (o podría cambiarse más adelante) insertar en otras tablas que tienen sus propias columnas IDENTITY, lo que estropearía el valor devuelto. Esto también frustraría SCOPE_IDENTITY(), si fuera posible.
  • Y finalmente, no pudo usar la cláusula OUTPUT (o un conjunto de resultados de una segunda consulta de la pseudotabla insertada después de la eventual inserción) dentro del disparador, porque esta capacidad requiere una configuración global y ha quedado obsoleta desde entonces. SQL Server 2005. Comprensiblemente, el código de Kendal debe ser compatible con versiones anteriores y, cuando sea posible, no depender completamente de ciertas bases de datos o configuraciones del servidor.

Entonces, volvamos a la realidad de Kendal. Su código parece lo suficientemente seguro, después de todo, está en una transacción; ¿qué puede salir mal? Bueno, echemos un vistazo a algunas oraciones importantes de la documentación IDENT_CURRENT (énfasis mío, porque estas advertencias están ahí por una buena razón):

Devuelve el último valor de identidad generado para una tabla o vista especificada. El último valor de identidad generado puede ser para cualquier sesión y cualquier alcance .

Tenga cuidado al usar IDENT_CURRENT para predecir el siguiente valor de identidad generado. El valor generado real puede ser diferente de IDENT_CURRENT más IDENT_INCR debido a las inserciones realizadas por otras sesiones .

Las transacciones apenas se mencionan en el cuerpo del documento (solo en el contexto de falla, no de concurrencia), y no se usan transacciones en ninguna de las muestras. Por lo tanto, probemos lo que estaba haciendo Kendal y veamos si podemos hacer que falle cuando se ejecutan varias sesiones al mismo tiempo. Voy a crear una tabla de registro para realizar un seguimiento de los valores generados por cada sesión, tanto el valor de identidad que realmente se generó (usando un disparador posterior) como el valor que se afirma que se generó de acuerdo con IDENT_CURRENT().

Primero, las tablas y disparadores:

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

Ahora, abra un puñado de ventanas de consulta y pegue este código, ejecutándolos lo más cerca posible para garantizar la mayor superposició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

Una vez que se hayan completado todas las ventanas de consulta, ejecute esta consulta para ver algunas filas aleatorias donde IDENT_CURRENT devolvió el valor incorrecto y un recuento de cuántas filas en total se vieron afectadas por este número mal informado:

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

Aquí están mis 10 filas para una prueba:

Me sorprendió que casi un tercio de las filas estuvieran apagadas. Sin duda, sus resultados variarán y pueden depender de la velocidad de sus unidades, el modelo de recuperación, la configuración del archivo de registro u otros factores. En dos máquinas diferentes, tuve índices de fallas muy diferentes, por un factor de 10 (una máquina más lenta solo tuvo alrededor de 10 000 fallas, o aproximadamente el 3 %).

Inmediatamente queda claro que una transacción no es suficiente para evitar que IDENT_CURRENT extraiga los valores de IDENTIDAD generados por otras sesiones. ¿Qué tal una transacción SERIALIZABLE? Primero, borre las dos tablas:

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

Luego, agregue este código al comienzo de la secuencia de comandos en varias ventanas de consulta y ejecútelos nuevamente de la forma más simultánea posible:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Esta vez, cuando ejecuto la consulta en la tabla IdentityLog, muestra que SERIALIZABLE puede haber ayudado un poco, pero no ha resuelto el problema:

Y aunque lo incorrecto es incorrecto, según los resultados de mi muestra, el valor IDENT_CURRENT generalmente solo tiene una diferencia de uno o dos. Sin embargo, esta consulta debería dar como resultado que puede estar *muy* fuera de lugar. En mis ejecuciones de prueba, este resultado fue tan alto como 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';

A través de esta evidencia, podemos concluir que IDENT_CURRENT no es seguro para transacciones. Parece una reminiscencia de un problema similar pero casi opuesto, donde las funciones de metadatos como OBJECT_NAME() se bloquean, incluso cuando el nivel de aislamiento es READ UNCOMMITTED, porque no obedecen la semántica de aislamiento circundante. (Consulte el artículo de conexión n.° 432497 para obtener más detalles).

Superficialmente, y sin saber mucho más sobre la arquitectura y la(s) aplicación(es), no tengo una sugerencia realmente buena para Kendal; Solo sé que IDENT_CURRENT *no* es la respuesta. :-) Simplemente no lo use. Por nada. Alguna vez. Para cuando lea el valor, ya podría estar equivocado.