Por que essa estimativa de cardinalidade de junção é tão grande?

18

Estou com o que considero uma estimativa de cardinalidade impossivelmente alta para a seguinte consulta:

SELECT dm.PRIMARY_ID
FROM
(
    SELECT COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst ON dm.PRIMARY_ID = lst.JOIN_ID;

O plano estimado está aqui . Estou trabalhando em uma cópia estatística das tabelas para não poder incluir um plano real. No entanto, não acho que seja muito relevante para esse problema.

O SQL Server estima que 481577 linhas serão retornadas da tabela derivada "dm". Em seguida, estima que 4528030000 linhas serão retornadas após a associação a X_LAST_TABLE, mas JOIN_ID é a chave primária de X_LAST_TIME. Eu esperaria uma estimativa de cardinalidade de junção entre 0 e 481577 linhas. Em vez disso, a estimativa de linha parece ser 10% do número de linhas que eu obteria ao unir as tabelas externa e interna. A matemática para isso funciona com o arredondamento: 481577 * 94025 * 0,1 = 45280277425, que é arredondado para 4528030000.

Estou procurando principalmente uma causa raiz para esse comportamento. Também estou interessado em soluções simples, mas não sugira alterar o modelo de dados ou usar tabelas temporárias. Esta consulta é uma simplificação da lógica em uma exibição. Eu sei que fazer COALESCE em algumas colunas e unir-se a elas não é uma boa prática. Parte do objetivo desta pergunta é descobrir se preciso recomendar que o modelo de dados seja redesenhado.

Estou testando no Microsoft SQL Server 2014 com o estimador de cardinalidade herdado ativado. O TF 4199 e outros estão ativados. Posso fornecer uma lista completa de sinalizadores de rastreamento, se isso for relevante.

Aqui está a definição de tabela mais relevante:

CREATE TABLE X_LAST_TABLE (
JOIN_ID NUMERIC(18, 0) NOT NULL
    CONSTRAINT PK_X_LAST_TABLE PRIMARY KEY CLUSTERED (JOIN_ID ASC)
);

Eu também escrevi todos os scripts de criação de tabela, juntamente com suas estatísticas, se alguém quiser reproduzir o problema em um de seus servidores.

Para adicionar algumas das minhas observações, o uso do TF 2312 corrige a estimativa, mas isso não é uma opção para mim. O TF 2301 não fixa a estimativa. A remoção de uma das tabelas corrige a estimativa. Estranhamente, alterar a ordem de junção de X_DETAIL_LINK também corrige a estimativa. Alterando a ordem de junção, refiro-me a reescrever a consulta e não forçar a ordem de junção com uma dica. Aqui está um plano de consulta estimado ao alterar apenas a ordem das junções.

Joe Obbish
fonte
PS Se você pode em qualquer switch maneira de bigint, em vez de decimal(18, 0)você poderá obter benefícios: 1) uso 8 bytes em vez de 9 para cada valor, e 2) o uso de um tipo de dados byte-comparável, em vez de um tipo de dados embalados, o que poderia ter implicações para o tempo da CPU ao comparar valores.
ErikE
@ ErikE Obrigado pela dica, mas eu já sabia disso. Infelizmente, estamos presos a NUMERIC (18,0) sobre BIGINT por motivos legados.
Joe Obbish
Valeu a pena!
31417 ErikE
Você precisa das tabelas X_DETAIL2e X_DETAIL3se JOIN_IDnão estiver nulo X_DETAIL1?
31517 ErikE
@ ErikE Este é um MCVE, portanto a consulta não faz exatamente sentido neste momento.
Joe Obbish

Respostas:

14

Eu sei que fazer COALESCEem algumas colunas e ingressar nelas não é uma boa prática.

A geração de boas estimativas de cardinalidade e distribuição já é bastante difícil quando o esquema é 3NF + (com chaves e restrições) e a consulta é relacional e principalmente SPJG (seleção-projeção-junção por grupo). O modelo CE é construído sobre esses princípios. Quanto mais recursos incomuns ou não relacionais houver em uma consulta, mais se aproximará dos limites com os quais a estrutura de cardinalidade e seletividade pode lidar. Vá longe demais e a CE vai desistir e adivinhar .

A maior parte do exemplo do MCVE é SPJ simples (sem G), embora com equijoins predominantemente externos (modelados como junção interna mais anti-semijoína), em vez do equijoin interno mais simples (ou semijoína). Todas as relações têm chaves, embora nenhuma chave estrangeira ou outras restrições. Todas as uniões, exceto uma, são uma para muitas, o que é bom.

A exceção é a junção externa muitos para muitos entre X_DETAIL_1e X_DETAIL_LINK. A única função dessa associação no MCVE é potencialmente duplicar linhas X_DETAIL_1. Esse é um tipo incomum de coisa.

Predicados de igualdade simples (seleções) e operadores escalares também são melhores. Por exemplo, atributo / constante de comparação igual ao atributo normalmente funciona bem no modelo. É relativamente "fácil" modificar histogramas e estatísticas de frequência para refletir a aplicação de tais predicados.

COALESCEé construído sobre CASE, que por sua vez é implementado internamente como IIF(e isso era verdade muito antes de IIFaparecer na linguagem Transact-SQL). O CE modela IIFcomo a UNIONcom dois filhos mutuamente exclusivos, cada um consistindo em um projeto em uma seleção na relação de entrada. Cada um dos componentes listados possui suporte ao modelo, portanto, combiná-los é relativamente simples. Mesmo assim, quanto mais camadas as abstrações, menos preciso o resultado final tende a ser - uma razão pela qual planos de execução maiores tendem a ser menos estáveis ​​e confiáveis.

ISNULL, por outro lado, é intrínseco ao mecanismo. Não é construído usando mais componentes básicos. A aplicação do efeito de ISNULLum histograma, por exemplo, é tão simples quanto substituir a etapa por NULLvalores (e compactação conforme necessário). Ainda é relativamente opaco, como os operadores escalares, e é melhor evitar sempre que possível. No entanto, geralmente é mais amigável ao otimizador (menos hostil ao otimizador) do que uma CASEalternativa baseada em.

O CE (70 e 120+) é muito complexo, mesmo para os padrões do SQL Server. Não se trata de aplicar lógica simples (com uma fórmula secreta) a cada operador. O CE conhece chaves e dependências funcionais; sabe estimar usando frequências, estatísticas multivariadas e histogramas; e há uma tonelada absoluta de casos especiais, refinamentos, freios e contrapesos e estruturas de apoio. Geralmente estima, por exemplo, junções de várias maneiras (frequência, histograma) e decide um resultado ou ajuste com base nas diferenças entre os dois.

Uma última coisa básica a ser abordada: a estimativa inicial da cardinalidade é executada para todas as operações na árvore de consultas, de baixo para cima. A seletividade e a cardinalidade são derivadas primeiro para os operadores de folha (relações de base). Os histogramas modificados e as informações de densidade / frequência são derivados para os operadores principais. Quanto mais subimos na árvore, menor é a qualidade das estimativas, pois os erros tendem a se acumular.

Essa estimativa abrangente única inicial fornece um ponto de partida e ocorre bem antes de qualquer consideração ser dada a um plano de execução final (isso ocorre muito antes do estágio trivial de compilação do plano). A árvore de consultas neste momento tende a refletir bastante a forma escrita da consulta (embora com subconsultas removidas e simplificações aplicadas etc.)

Imediatamente após a estimativa inicial, o SQL Server executa o reordenamento de junção heurística, que tenta falar livremente a árvore para colocar tabelas menores e as junções de alta seletividade primeiro. Ele também tenta posicionar as junções internas antes das junções externas e cruzar produtos. Suas capacidades não são extensas; seus esforços não são exaustivos; e não considera custos físicos (já que eles ainda não existem - apenas informações estatísticas e metadados estão presentes). O reordenamento heurístico é mais bem-sucedido em simples árvores equijóicas internas. Existe para fornecer um "ponto de partida" melhor para a otimização baseada em custos.

Por que essa estimativa de cardinalidade de junção é tão grande?

O MCVE tem uma junção muitos-para-muitos "incomum", principalmente redundante , e uma junção equi COALESCEno predicado. A árvore do operador também possui uma união interna por último , cujo reordenamento de união heurística não conseguiu mover a árvore para uma posição mais preferida. Deixando de lado todos os escalares e projeções, a árvore de junção é:

LogOp_Join [ Card=4.52803e+009 ]
    LogOp_LeftOuterJoin [ Card=481577 ]
        LogOp_LeftOuterJoin [ Card=481577 ]
            LogOp_LeftOuterJoin [ Card=481577 ]
                LogOp_LeftOuterJoin [ Card=481577 ]
                LogOp_Get TBL: X_DRIVING_TABLE(alias TBL: dt) [ Card=481577 ]
                LogOp_Get TBL: X_DETAIL_1(alias TBL: d1) [ Card=70 ]
                LogOp_Get TBL: X_DETAIL_LINK(alias TBL: lnk) [ Card=47 ]
            LogOp_Get TBL: X_DETAIL_2(alias TBL: d2) X_DETAIL_2 [ Card=119 ]
        LogOp_Get TBL: X_DETAIL_3(alias TBL: d3) X_DETAIL_3 [ Card=281 ]
    LogOp_Get TBL: X_LAST_TABLE(alias TBL: lst) X_LAST_TABLE [ Card=94025 ]

Observe que a estimativa final com defeito já está em vigor. É impresso Card=4.52803e+009e armazenado internamente como o valor de ponto flutuante de precisão dupla 4.5280277425e + 9 (4528027742.5 em decimal).

A tabela derivada na consulta original foi removida e as projeções normalizadas. Uma representação SQL da árvore na qual a estimativa inicial de cardinalidade e seletividade foi realizada é:

SELECT 
    PRIMARY_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)
FROM X_DRIVING_TABLE dt
LEFT OUTER JOIN X_DETAIL_1 d1
    ON dt.ID = d1.ID
LEFT OUTER JOIN X_DETAIL_LINK lnk 
    ON d1.LINK_ID = lnk.LINK_ID
LEFT OUTER JOIN X_DETAIL_2 d2 
    ON dt.ID = d2.ID
LEFT OUTER JOIN X_DETAIL_3 d3 
    ON dt.ID = d3.ID
INNER JOIN X_LAST_TABLE lst 
    ON lst.JOIN_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)

(Além disso, o repetido COALESCEtambém está presente no plano final - uma vez no escalar computacional final e outra no lado interno da junção interna).

Observe a junção final. Essa junção interna é (por definição) o produto cartesiano X_LAST_TABLEe a saída da junção anterior, com uma seleção (predicado de junção) lst.JOIN_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)aplicada. A cardinalidade do produto cartesiano é simplesmente 481577 * 94025 = 45280277425.

Para isso, precisamos determinar e aplicar a seletividade do predicado. A combinação da COALESCEárvore expandida opaca (em termos de , UNIONe IIFlembre-se), juntamente com o impacto nas informações-chave, histogramas e frequências derivadas da junção externa "incomum", geralmente redundante de muitos para muitos, significa que o CE é incapaz de derivar uma estimativa aceitável de qualquer maneira normal.

Como resultado, ele entra na Guess Logic. A lógica de adivinhação é moderadamente complexa, com tentativas de camadas de adivinhações "instruídas" e "não tão educadas". Se não for encontrada uma base melhor para uma suposição, o modelo usa uma suposição de último recurso, que para uma comparação de igualdade é: sqllang!x_Selectivity_Equal= 0,1 seletividade fixa (10% de suposição):

Pilha de chamadas

-- the moment of doom
movsd xmm0,mmword ptr [sqllang!x_Selectivity_Equal

O resultado é seletividade de 0,1 no produto cartesiano: 481577 * 94025 * 0,1 = 4528027742,5 (~ 4,52803e + 009) conforme mencionado anteriormente.

Reescreve

Quando a junção problemática é comentada , uma estimativa melhor é produzida porque a seletividade fixa "palpite de último recurso" é evitada (as principais informações são retidas pelas junções 1-M). A qualidade da estimativa ainda é pouco confiável, porque um COALESCEpredicado de junção não é nada amigável para o CE. A estimativa revisada faz pelo menos olhar mais razoável para os seres humanos, suponho.

Quando a consulta é gravada com a junção externa a X_DETAIL_LINK última colocada , o reordenamento heurístico pode trocá-la pela junção interna final para X_LAST_TABLE. Colocar a junção interna logo ao lado da junção externa do problema dá às habilidades limitadas de reordenar cedo a oportunidade de melhorar a estimativa final, uma vez que os efeitos da junção externa "incomum" muitos-para-muitos "redundante" são redundantes após a complicada estimativa de seletividade para COALESCE. Mais uma vez, as estimativas são um pouco melhores do que suposições fixas e provavelmente não resistiriam a um interrogatório determinado em um tribunal.

Reordenar uma mistura de junções internas e externas é demorado e demorado (mesmo a otimização completa do estágio 2 tenta apenas um subconjunto limitado de movimentos teóricos).

O aninhado ISNULLsugerido na resposta de Max Vernon consegue evitar o palpite fixo de resgate, mas a estimativa final é de improvável zero linhas (aumentada para uma linha por decência). Isso também pode ser um palpite fixo de 1 linha, para toda a base estatística que o cálculo tiver.

Eu esperaria uma estimativa de cardinalidade de junção entre 0 e 481577 linhas.

Essa é uma expectativa razoável, mesmo que se aceite que a estimativa de cardinalidade pode ocorrer em momentos diferentes (durante a otimização baseada em custos) em subárvores fisicamente diferentes, mas lógica e semanticamente idênticas - com o plano final sendo uma espécie de melhor costura melhor (por grupo de notas). A falta de uma garantia de consistência em todo o plano não significa que uma junção individual deva desprezar a respeitabilidade, entendo isso.

Por outro lado, se acabarmos com o palpite de último recurso , a esperança já está perdida, então, por que se preocupar? Tentamos todos os truques que conhecíamos e desistimos. Se nada mais, a estimativa final é um ótimo sinal de alerta de que nem tudo correu bem dentro do CE durante a compilação e otimização dessa consulta.

Quando tentei o MCVE, o 120+ CE produziu uma estimativa final de linha zero (= 1) (como a aninhada ISNULL) para a consulta original, o que é igualmente inaceitável para o meu modo de pensar.

A solução real provavelmente envolve uma alteração no design, para permitir junções equitativas simples sem COALESCEou ISNULL, idealmente, chaves estrangeiras e outras restrições úteis para a compilação de consultas.

Paul White restabelece Monica
fonte
10

Acredito que o Compute Scalaroperador resultante da COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)associação X_LAST_TABLE.JOIN_IDseja a causa raiz do problema. Historicamente, os escalares de computação têm sido difíceis de custar com precisão 1 , 2 .

Como você forneceu um exemplo verificável minimamente completo (obrigado!) Com estatísticas precisas, sou capaz de reescrever a consulta para que a junção não exija mais a CASEfuncionalidade COALESCEexpandida, resultando em estimativas de linha muito mais precisas e aparentemente mais custo total preciso Veja o adendo no final. :

SELECT COALESCE(dm.d1ID, dm.d2ID, dm.d3ID)
FROM
(
    SELECT d1ID = d1.JOIN_ID
        , d2ID = d2.JOIN_ID
        , d3ID = d3.JOIN_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst 
    ON (dm.d1ID IS NOT NULL AND dm.d1ID = lst.JOIN_ID)
    OR (dm.d1ID IS NULL AND dm.d2ID IS NOT NULL AND dm.d2ID = lst.JOIN_ID)
    OR (dm.d1ID IS NULL AND dm.d2ID IS NULL AND dm.d3ID IS NOT NULL AND dm.d3ID = lst.JOIN_ID);

Embora xID IS NOT NULLtecnicamente não seja necessário, pois o valor ID = JOIN_IDnão será associado a valores nulos, eu os incluí, pois retrata com mais clareza a intenção.

Plano 1 e Plano 2

Plano 1:

insira a descrição da imagem aqui

Plano 2:

insira a descrição da imagem aqui

A nova consulta se beneficia (?) Da paralelização. Observe também que a nova consulta possui um número de saída estimado de linhas 1, que pode ser pior no final do dia do que a estimativa de 4528030000 para a consulta original. O custo da subárvore para o operador de seleção na nova consulta é 243210, enquanto o original é 536.535, o que é claramente menor. Dito isto, não acredito que a primeira estimativa esteja próxima da realidade.


Adenda 1.

Após uma consulta mais aprofundada com várias pessoas no The Heap ™, estimulada por uma discussão com o @Lamak, parece que minha consulta observacional acima tem um desempenho terrível, mesmo com o paralelismo. Uma solução que permite bom desempenho e boas estimativas de cardinalidade consiste em substituir o COALESCE(x,y,z)por um ISNULL(ISNULL(x, y), z), como em:

SELECT dm.PRIMARY_ID
FROM
(
    SELECT ISNULL(ISNULL(d1.JOIN_ID, d2.JOIN_ID), d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst ON dm.PRIMARY_ID = lst.JOIN_ID;

COALESCEé transformado em uma CASEdeclaração "oculta" pelo otimizador de consulta. Dessa forma, o estimador de cardinalidade tem mais dificuldade em descobrir estatísticas confiáveis ​​para colunas enterradas no interior COALESCE. ISNULLser uma função intrínseca é muito mais "aberta" para o estimador de cardinalidade. Também não vale nada que ISNULLpossa ser otimizado se o destino for conhecido como não nulo.

O plano para a ISNULLvariante se parece com:

insira a descrição da imagem aqui

(Cole a versão do plano aqui ).

Para sua informação, apóie o Sentry One por seu ótimo Plan Explorer, que eu usei para produzir os planos gráficos acima.

Max Vernon
fonte
-1

De acordo com a sua condição de associação, a tabela pode ser organizada de tantas maneiras, que "mudar de uma maneira específica" corrige o resultado.

Suponha que juntar apenas uma tabela lhe dê o resultado correto.

SELECT COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID

Aqui no lugar de X_DETAIL_1, você pode usar X_DETAIL_2ou X_DETAIL_3.

Por isso, o objetivo do resto 2 tabelas não é claro.

É como se você tivesse quebrado a mesa X_DETAIL_1em mais duas partes.

Muito provavelmente " não há erro em que você está preenchendo essas tabelas. " Idealmente X_DETAIL_1, X_DETAIL_2e X_DETAIL_3deve conter a mesma quantidade de linhas.

Mas uma ou mais tabelas contêm um número indesejado de linhas.

Desculpe se estou errado.

KumarHarsh
fonte