Database
 sql >> Base de Dados >  >> RDS >> Database

Pela última vez, NÃO, você não pode confiar em IDENT_CURRENT()


Eu tive uma discussão ontem com Kendal Van Dyke (@SQLDBA) sobre IDENT_CURRENT(). Basicamente, Kendal tinha este código, que ele testou e confiou por conta própria, e queria saber se ele poderia confiar que IDENT_CURRENT() fosse preciso em um ambiente simultâneo de alta escala:
BEGIN TRANSACTION;
INSERT dbo.TableName(ColumnName) VALUES('Value');
SELECT IDENT_CURRENT('dbo.TableName');
COMMIT TRANSACTION;

A razão pela qual ele teve que fazer isso é porque ele precisa retornar o valor IDENTITY gerado para o cliente. As maneiras típicas de fazer isso são:
  • SCOPE_IDENTITY()
  • cláusula OUTPUT
  • @@IDENTIDADE
  • IDENT_CURRENT()

Alguns destes são melhores do que outros, mas isso foi feito até a morte, e não vou entrar nisso aqui. No caso de Kendal, IDENT_CURRENT foi seu último e único recurso, porque:
  • TableName tinha um gatilho INSTEAD OF INSERT, tornando SCOPE_IDENTITY() e a cláusula OUTPUT inúteis do chamador, porque:
    • SCOPE_IDENTITY() retorna NULL, pois a inserção realmente aconteceu em um escopo diferente
    • a cláusula OUTPUT gera o erro Msg 334 por causa do gatilho
  • Ele eliminou @@IDENTITY; considere que o gatilho INSTEAD OF INSERT pode agora (ou pode ser alterado para) inserir em outras tabelas que tenham suas próprias colunas IDENTITY, o que atrapalharia o valor retornado. Isso também impediria SCOPE_IDENTITY(), se fosse possível.
  • E, finalmente, ele não poderia usar a cláusula OUTPUT (ou um conjunto de resultados de uma segunda consulta da pseudotabela inserida após a eventual inserção) dentro do gatilho, porque esse recurso requer uma configuração global e foi preterido desde SQL Server 2005. Compreensivelmente, o código de Kendal precisa ser compatível com versões futuras e, quando possível, não depender completamente de determinadas configurações de banco de dados ou servidor.

Então, de volta à realidade de Kendal. Seu código parece bastante seguro – afinal, está em uma transação; o que poderia dar errado? Bem, vamos dar uma olhada em algumas frases importantes da documentação IDENT_CURRENT (ênfase minha, porque esses avisos estão lá por um bom motivo):
Retorna o último valor de identidade gerado para uma tabela ou exibição especificada. O último valor de identidade gerado pode ser para qualquer sessão e qualquer escopo .



Tenha cuidado ao usar IDENT_CURRENT para prever o próximo valor de identidade gerado. O valor gerado real pode ser diferente de IDENT_CURRENT mais IDENT_INCR por causa de inserções realizadas por outras sessões .
As transações são mal mencionadas no corpo do documento (somente no contexto de falha, não de simultaneidade), e nenhuma transação é usada em nenhuma das amostras. Então, vamos testar o que Kendal estava fazendo e ver se podemos fazer com que ele falhe quando várias sessões estiverem sendo executadas simultaneamente. Vou criar uma tabela de log para acompanhar os valores gerados por cada sessão – tanto o valor de identidade que foi realmente gerado (usando um acionador after), quanto o valor alegado para ser gerado de acordo com IDENT_CURRENT().

Primeiro, as tabelas e gatilhos:
-- 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

Agora, abra algumas janelas de consulta e cole este código, executando-as o mais próximo possível para garantir a maior sobreposição:
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

Depois que todas as janelas de consulta forem concluídas, execute esta consulta para ver algumas linhas aleatórias em que IDENT_CURRENT retornou o valor errado e uma contagem de quantas linhas no total foram afetadas por esse número informado incorretamente:
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();

Aqui estão minhas 10 linhas para um teste:



Achei surpreendente que quase um terço das fileiras estivesse fora. Seus resultados certamente variam e podem depender da velocidade de suas unidades, modelo de recuperação, configurações do arquivo de log ou outros fatores. Em duas máquinas diferentes, tive taxas de falhas muito diferentes – por um fator de 10 (uma máquina mais lenta tinha apenas cerca de 10.000 falhas, ou cerca de 3%).

Imediatamente fica claro que uma transação não é suficiente para impedir que IDENT_CURRENT puxe os valores de IDENTITY gerados por outras sessões. Que tal uma transação SERIALIZÁVEL? Primeiro, limpe as duas tabelas:
TRUNCATE TABLE dbo.TableName;
TRUNCATE TABLE dbo.IdentityLog;

Em seguida, adicione este código ao início do script em várias janelas de consulta e execute-as novamente o mais simultaneamente possível:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Desta vez, quando executo a consulta na tabela IdentityLog, mostra que SERIALIZABLE pode ter ajudado um pouco, mas não resolveu o problema:



E embora errado seja errado, parece dos meus resultados de amostra que o valor IDENT_CURRENT geralmente está apenas um ou dois errados. No entanto, esta consulta deve indicar que pode ser *way* off. Em minhas execuções de teste, esse resultado foi tão alto quanto 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';

Por meio dessa evidência, podemos concluir que IDENT_CURRENT não é seguro para transações. Parece uma reminiscência de um problema semelhante, mas quase oposto, em que funções de metadados como OBJECT_NAME() são bloqueadas – mesmo quando o nível de isolamento é READ UNCOMMITTED – porque não obedecem à semântica de isolamento circundante. (Consulte o item de conexão nº 432497 para obter mais detalhes.)

Na superfície, e sem saber muito mais sobre a arquitetura e aplicações, não tenho uma sugestão muito boa para Kendal; Só sei que IDENT_CURRENT *não* é a resposta. :-) Só não use. Para qualquer coisa. Sempre. Quando você ler o valor, já pode estar errado.