Tenho um teste sintético, que reproduz algum erro que temos no ambiente de produção. Aqui estão os 2 scripts para reproduzi-lo:
1º
DBCC TRACEOFF(-1,3604,1200) WITH NO_INFOMSGS;
SET NOCOUNT ON;
IF @@TRANCOUNT > 0 ROLLBACK
IF object_id('test') IS NOT NULL
DROP TABLE test
IF object_id('TMP_test') IS NOT NULL
DROP TABLE TMP_test
IF object_id('test1') IS NOT NULL
DROP TABLE test1
CREATE TABLE test(Id INT IDENTITY PRIMARY KEY)
GO
INSERT test DEFAULT VALUES
GO 2000
WHILE 1 = 1
BEGIN
CREATE TABLE TMP_test(Id INT PRIMARY KEY)
INSERT TMP_test SELECT * FROM test
WAITFOR DELAY '0:00:00.1'
BEGIN TRAN
EXEC sp_rename 'test', 'test1'
EXEC sp_rename 'TMP_test', 'test'
EXEC sp_rename 'test1', 'TMP_test'
DROP TABLE TMP_test
commit
END
2º
SET NOCOUNT ON;
DECLARE @c INT
WHILE 1 = 1
BEGIN
SELECT @c = COUNT(*) FROM Test IF @@ERROR <> 0 BREAK
SELECT @c = COUNT(*) FROM Test IF @@ERROR <> 0 BREAK
/* and repeat this 10-20 times more*/
SELECT @c = COUNT(*) FROM Test IF @@ERROR <> 0 BREAK
END
Então, o problema é que quando executo o 1º script em uma sessão e deixo rodando, e depois executo o 2º em sessão separada, posso obter este tipo de erro:
Msg 208, Nível 16, Estado 1, Linha 13 Nome de objeto inválido 'Teste'.
A questão é - POR QUE vejo esse erro no caso de haver um COMMIT
no final do ciclo do primeiro script e nunca recebo um se houver um ROLLBACK
?
Tenho a sensação de que está de alguma forma conectado à situação, quando o script é confirmado, ainda há a tabela com o nome test
, mas é um objeto diferente e o segundo script precisa se recompilar. E tudo bem. Mas por que ele recebe o erro de tabela perdida? AFAIK - quando eu renomeio a tabela dentro de uma transação - ela mantém o bloqueio Sch-M no final da transação?
Alguém pode responder ou me guiar para os artigos técnicos que posso ler profundamente e entender o motivo?
Pergunta realmente interessante e difícil. Não consegui encontrar nenhuma documentação oficial desse comportamento e suspeito que não haja nenhuma (embora eu adoraria se alguém me corrigisse sobre isso!).
Minha pesquisa me leva a acreditar que é a etapa de compilação do plano que é vulnerável a essa condição de corrida. Observe que consegui executar suas consultas de teste por uma hora sem erros, mas se eu iniciar seu processo repetidamente, recebo um erro imediato em uma parte do tempo. Quando ocorre um erro, sempre o faz imediatamente na compilação do plano. Como alternativa, você pode adicionar "OPTION RECOMPILE" ao COUNT(*) no loop, forçando a compilação de um novo plano a cada tentativa. Usando essa abordagem, vejo o erro quase imediatamente toda vez que executo seus scripts.
Consegui reproduzir o erro com uma série controlada de etapas que parece atingir o erro a cada tentativa, eliminando a necessidade de configurar um loop e confiar na aleatoriedade.
Eu também lancei uma correção potencial (para usar ALTER TABLE...SWITCH) que pode ser possível experimentar em seu ambiente de produção. Agora, vamos aos detalhes!
Aqui estão os passos para reproduzir:
Usando essas etapas, podemos nos aprofundar um pouco mais no que está causando o erro. Para isso, executei um rastreamento com os seguintes eventos em: SP:StmtStarting, SP:StmtCompleted, Lock:Acquired, Lock:Released.
O que descobri é que ocorre o seguinte, em ordem (mas omitindo alguns detalhes intermediários):
No entanto, se uma sequência semelhante de eventos ocorrer quando uma compilação de plano não for necessária (como ao executar seu loop), o SQL Server será realmente inteligente o suficiente para detectar que o object_id foi alterado. Também rastreei esse caso e encontrei uma situação em que o segundo COUNT(*) executou a seguinte sequência
Portanto, parece que essa lógica adaptativa está em vigor, como você hipotetizou. Mas parece que pode estar em vigor apenas para execução de consulta e não para compilação de consulta.
Conforme prometido, aqui está um trecho alternativo para a seção (3) que parece fornecer uma maneira de renomear tabelas que resolve o problema de simultaneidade (pelo menos na minha máquina!):
Por fim, aqui está outro link que achei útil para entender como pensar sobre o bloqueio no contexto de tabelas cujos nomes estão mudando: