Estou migrando alguns dados armazenados no estilo "valor-chave", de uma tabela de atributos que usa um ID de objeto e um tipo de atributo como um índice clusterizado (também tentei como um índice não clusterizado):
CREATE TABLE [dbo].[#attrs](
[DataMigrationEventObjectID] [int] NOT NULL,
[AttributeType] [varchar](128) NOT NULL,
[AttributeValue] [varchar](255) NULL
)
CREATE CLUSTERED INDEX pk ON #attrs ([DataMigrationEventObjectID],AttributeType);
Adicionei o valor do atributo para selecionar valores porque a tabela de atributos no banco de dados tem muitos outros dados e posso selecioná-los apenas para este evento de migração. A consulta com meu conjunto de dados de teste para preencher esta tabela insere ~ 3k linhas e é executada em menos de um segundo (existem cerca de 50 objetos no total em meu conjunto de dados com vários atributos cada).
As junções à tabela na consulta têm esta aparência, juntando-se no índice clusterizado:
INNER JOIN #attrs obj_gvn
ON obj_gvn.DataMigrationEventObjectID = obj.DataMigrationEventObjectID
AND obj_gvn.AttributeType = 'GivenName'
Com 14 junções a esta tabela temporária, a consulta é concluída em alguns segundos. Com 15 junções, a consulta leva um minuto e, com 16+, ainda está sendo executada meia hora depois.
Verifiquei todas as junções em busca de condições acidentais que levariam ao retorno de muitas linhas e, quando ele retorna em 1 minuto, retorna apenas as linhas certas, então não acho que haja uma junção cartesiana acidental. Definir um valor MAXDOP não o afeta e o plano de consulta retornado quando a consulta é executada por um minuto não sinaliza nenhum problema.
O que eu perdi no SQL que faz com que ele se comporte dessa maneira com muitas junções em um índice clusterizado que em teoria deveria ser rápido, com um número tão pequeno de registros?
Não consigo obter um plano de execução real porque a consulta não é concluída e, como usa uma tabela temporária, não consigo obter um plano estimado para ela. Tentei falsificar a tabela temporária como uma tabela real no banco de dados e gerar um plano estimado e o plano ainda não foi gerado após 2 minutos, então parece que o atraso está no lado "criando um plano"
Cole o Plano de uma versão abreviada da consulta: brentozar.com/pastetheplan/?id=Hy76dd92i
Atualizei as estatísticas do banco de dados para garantir e ainda não está gerando um plano.
Eu trabalhei com consultas no passado com junções muito mais e muito mais problemáticas, onde a compilação do plano ainda é instantânea. Eu sinto que o fato de estar falhando na etapa "gerar plano" deve significar algo.
Atualizar para o CU mais recente infelizmente não ajudou. sp_whoisactive
apenas mostra o uso da CPU subindo e subindo ( captura de tela ), nada em outros recursos que pareça problemático.
É minha máquina de desenvolvimento, portanto, há apenas um processo ativo no SQL, que é a consulta que estou executando. Não há mais nada, então presumo que seja o SQL tentando gerar o plano.
Suspeito que, se eu implantasse isso, a produção funcionaria bem, mas é muito estranho ter um problema de 'muitas junções em um desempenho de eliminação de chave primária'. Posso pensar em acabar com o servidor de desenvolvimento e começar do zero.
Em geral
O tempo de compilação de uma consulta com um grande número de junções pode ser muito variável.
Uma razão para isso é que existem N! maneiras de ordenar as junções internas de N tabelas. Para N pequeno, isso não é um problema. Para N maior pode ser um problema, mesmo que o otimizador não tente uma busca exaustiva do possível espaço de planta.
Você deve ter notado que o plano de execução fornecido não acessa as tabelas na mesma ordem em que as escreveu. Por exemplo, a tabela DataMigrationEventObject (aliasada como 'obj') é acessada primeiro. Não há um único índice óbvio disponível, portanto, o otimizador cria uma interseção de índice (usando dois índices não clusterizados) e uma pesquisa de chave (buscando a coluna TargetObjectKey1 ) para implementar fisicamente essa pequena parte de sua consulta.
O otimizador também não se limita a considerar diferentes ordens de junção. Ele pode explorar diferentes algoritmos de junção (hash, merge, nested loop, apply), diferentes estratégias de indexação, posicionamento e tipo de operadores sem junção, paralelismo e assim por diante.
O otimizador possui heurísticas para evitar gastar muito tempo procurando um plano melhor se o melhor que encontrou até agora for razoável. Dada uma consulta razoavelmente direta, um design lógico de banco de dados decente e uma implementação física adequada (por exemplo, tipos de dados e índices), o otimizador geralmente encontrará rapidamente um bom plano de baixo custo. Esta parece ter sido sua experiência no passado.
Esse arranjo ainda pode dar errado de várias maneiras. Se a ordem de junção inicial escolhida pelo otimizador estiver muito longe de ser um custo razoavelmente baixo, muito trabalho de transformação precisará ser feito para chegar à linha de chegada. Se o banco de dados não tiver bons índices de suporte, o otimizador pode gastar muito tempo tentando encontrar uma estratégia decente.
Se as informações de contagem de linhas e distribuição de valor não puderem ser derivadas com precisão (não se limitando a estatísticas de tabela base desatualizadas), os candidatos a fragmentos de plano poderão ser avaliados incorretamente. Isso pode facilmente levar à violação de limites heurísticos e ao otimizador gastar uma quantidade excessiva de esforço explorando, implementando e custeando alternativas.
Quando isso acontece, às vezes a única solução é expressar o requisito de dados usando uma sintaxe diferente, um algoritmo diferente (como na sugestão do pivô) ou dividir a operação em etapas separadas mais simples com tabelas temporárias pequenas e bem indexadas como retenção áreas.
Em geral, parece mais provável que o problema no seu caso seja uma distribuição de dados específica que leva a custos iniciais imprecisos e esforço excessivo do otimizador como resultado.
Especificamente
Sua consulta e banco de dados têm vários problemas que os tornam vulneráveis ao tempo excessivo de compilação do plano. Não vou listar todos eles porque um esquema completo não foi fornecido e o texto da consulta encontrado no plano de execução fornecido está truncado em um ponto crucial. A resposta seria muito longa mesmo se todas as informações necessárias fossem fornecidas. Alguns pontos:
#attrs
tabela temporária não está marcado comoUNIQUE
. As chaves são únicas por definição. Não fornecer essa garantia crucial ao otimizador significa que ele não pode saber que um predicado de igualdade no ID e tipo do objeto retornará no máximo uma linha. Também existem outras consequências, muitas para listar, mas, como exemplo, uma junção de mesclagem teria que ser do tipo muitos para muitos, em vez de um para muitos. Essas coisas são importantes para exploração e custeio.#attrs
tabela tem um índice duplicado chamado 'clus'. Isso foi mencionado na pergunta, mas é um exemplo de uma escolha de indexação não óbvia apresentada ao otimizador sem ganho. Torne o índice clusterizado umaUNIQUE
chave adequada.#attrs
tabela tem um predicado duplicadoDataMigrationEventID = @MigrationEventID
. Isso é inofensivo, mas a atenção aos detalhes é importante ao lidar com computadores (eles são muito literais).#theClients
tabela temporária usandoSELECT INTO
o retorno imediato de todos os resultados dessa tabela. As tabelas temporárias são uma poderosa ferramenta de ajuste quando usadas corretamente. Este não é um exemplo de tal uso.Reprodução
Para quem deseja gerar um plano localmente, consegui inferir o seguinte a partir da pergunta e do plano de execução. Pode haver algumas omissões e imprecisões. Naturalmente, não há estatísticas precisas, apenas uma cardinalidade de tabela bruta:
Tabelas
Consulta
Essa demonstração não reproduz o tempo de compilação excessivo em minha instância do SQL Server 2019 no nível de compatibilidade 130, mas isso não é inesperado, dada a falta de estatísticas precisas e provavelmente configuração diferente de hardware e instância (principalmente memória), o que afeta a escolha do plano.
Um tempo de compilação mais longo (e um plano bem maluco) pode ser reproduzido usando o modelo de cardinalidade original. Adicione a seguinte dica à consulta final:
Eu usei
MAXDOP 1
desde que sua instância está configurada dessa maneira.Não tenho certeza se isso resolverá seu problema, mas você já tentou dinamizar o dicionário #attrs e, em seguida, ingressar nele? Acho que, às vezes, adotar uma abordagem diferente pelo menos fará com que funcione. Algo como o abaixo.