Eu sei que fazer COALESCE
em 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_1
e 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 IIF
aparecer na linguagem Transact-SQL). O CE modela IIF
como a UNION
com 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 ISNULL
um histograma, por exemplo, é tão simples quanto substituir a etapa por NULL
valores (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 CASE
alternativa 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 COALESCE
no 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+009
e 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 COALESCE
també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_TABLE
e 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 , UNION
e IIF
lembre-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):
-- 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 COALESCE
predicado 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 ISNULL
sugerido 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 COALESCE
ou ISNULL
, idealmente, chaves estrangeiras e outras restrições úteis para a compilação de consultas.
bigint
, em vez dedecimal(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.