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.