Estou com dificuldade para entender por que o SQL Server apresentaria uma estimativa que pode ser tão facilmente comprovada como inconsistente com as estatísticas.
Consistência
Não há garantia geral de consistência. As estimativas podem ser calculadas em subárvores diferentes (mas logicamente equivalentes) em momentos diferentes, usando métodos estatísticos diferentes.
Não há nada errado com a lógica que diz que a união dessas duas subárvores idênticas deve produzir um produto cruzado, mas também não há nada a dizer que a escolha do raciocínio é mais sólida do que qualquer outra.
Estimativa inicial
No seu caso específico, a estimativa inicial de cardinalidade para a junção não é realizada em duas subárvores idênticas . A forma da árvore naquele momento é:
LogOp_Join
LogOp_GbAgg
LogOp_LeftOuterJoin
LogOp_Get TBL: ar
LogOp_Select
LogOp_Get TBL: tcr
ScaOp_Comp x_cmpEq
ScaOp_Identifier [tcr] .rId
Valor ScaOp_Const = 508
ScaOp_Logical x_lopAnd
ScaOp_Comp x_cmpEq
ScaOp_Identifier [ar] .fId
ScaOp_Identifier [tcr] .fId
ScaOp_Comp x_cmpEq
ScaOp_Identifier [ar] .bId
ScaOp_Identifier [tcr] .bId
AncOp_PrjList
AncOp_PrjEl Expr1003
ScaOp_AggFunc stopMax
ScaOp_Convert int
ScaOp_Identifier [tcr] .isS
LogOp_Select
LogOp_GbAgg
LogOp_LeftOuterJoin
LogOp_Get TBL: ar
LogOp_Select
LogOp_Get TBL: tcr
ScaOp_Comp x_cmpEq
ScaOp_Identifier [tcr] .rId
Valor ScaOp_Const = 508
ScaOp_Logical x_lopAnd
ScaOp_Comp x_cmpEq
ScaOp_Identifier [ar] .fId
ScaOp_Identifier [tcr] .fId
ScaOp_Comp x_cmpEq
ScaOp_Identifier [ar] .bId
ScaOp_Identifier [tcr] .bId
AncOp_PrjList
AncOp_PrjEl Expr1006
ScaOp_AggFunc stopMin
ScaOp_Convert int
ScaOp_Identifier [ar] .isT
AncOp_PrjEl Expr1007
ScaOp_AggFunc stopMax
ScaOp_Convert int
ScaOp_Identifier [tcr] .isS
ScaOp_Comp x_cmpEq
ScaOp_Identifier Expr1006
Valor ScaOp_Const = 1
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [ar] .fId
ScaOp_Identifier QCOL: [ar] .fId
A primeira entrada de junção teve um agregado não projetado simplificado, e a segunda entrada de junção tem o predicado t.isT = 1
pressionado abaixo dela, onde t.isT
está MIN(CONVERT(INT, ar.isT))
. Apesar disso, o cálculo da seletividade para o isT
predicado pode ser usado CSelCalcColumnInInterval
em um histograma:
CSelCalcColumnInInterval
Coluna: COL: Expr1006
Histograma carregado para a coluna QCOL: [ar] .isT das estatísticas com o ID 3
Seletividade: 4.85248e-005
Coleção de estatísticas gerada:
CStCollFilter (ID = 11, CARD = 1)
CStCollGroupBy (ID = 10, CARD = 20608)
CStCollOuterJoin (ID = 9, CARD = 20608 x_jtLeftOuter)
CStCollBaseTable (ID = 3, CARD = 20608 TBL: ar)
CStCollFilter (ID = 8, CARTÃO = 1)
CStCollBaseTable (ID = 4, CARTÃO = 28 TBL: tcr)
A expectativa (correta) é que 20.608 linhas sejam reduzidas a 1 linha por esse predicado.
Juntar estimativa
A questão agora é como as 20.608 linhas da outra entrada de junção corresponderão a essa linha:
LogOp_Join
CStCollGroupBy (ID = 7, CARD = 20608)
CStCollOuterJoin (ID = 6, CARD = 20608 x_jtLeftOuter)
...
CStCollFilter (ID = 11, CARD = 1)
CStCollGroupBy (ID = 10, CARD = 20608)
...
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [ar] .fId
ScaOp_Identifier QCOL: [ar] .fId
Existem várias maneiras diferentes de estimar a junção em geral. Poderíamos, por exemplo:
- Derive novos histogramas em cada operador de plano em cada subárvore, alinhe-os na junção (interpolando os valores das etapas conforme necessário) e veja como eles correspondem; ou
- Execute um alinhamento "grosseiro" mais simples dos histogramas (usando valores mínimos e máximos, não passo a passo); ou
- Calcule seletividades separadas apenas para as colunas de junção (na tabela base e sem filtragem) e adicione o efeito de seletividade dos predicados que não são de junção.
- ...
Dependendo do estimador de cardinalidade em uso e de algumas heurísticas, qualquer uma delas (ou uma variação) pode ser usada. Consulte o White Paper da Microsoft Otimizando seus planos de consulta com o SQL Server 2014 Cardinality Estimator para obter mais.
Erro?
Agora, conforme observado na pergunta, neste caso, a junção de coluna única 'simples' (on fId
) usa a CSelCalcExpressionComparedToExpression
calculadora:
Planeje o cálculo:
CSelCalcExpressionComparedToExpression [ar] .fId x_cmpEq [ar] .fId
Histograma carregado para a coluna QCOL: [ar] .bId das estatísticas com o ID 2
Histograma carregado para a coluna QCOL: [ar] .fId das estatísticas com o ID 1
Seletividade: 0
Este cálculo avalia que a união das 20.608 linhas com a 1 linha filtrada terá uma seletividade zero: nenhuma linha corresponderá (relatada como uma linha nos planos finais). Isso está errado? Sim, provavelmente há um erro no novo CE aqui. Pode-se argumentar que 1 linha corresponderá a todas ou nenhuma, portanto o resultado pode ser razoável, mas há motivos para acreditar em contrário.
Os detalhes são realmente bastante complicados, mas a expectativa da estimativa se basear em fId
histogramas não filtrados , modificados pela seletividade do filtro, fornecendo 20608 * 20608 * 4.85248e-005 = 20608
linhas é muito razoável.
Seguir esse cálculo significaria usar a calculadora em CSelCalcSimpleJoinWithDistinctCounts
vez de CSelCalcExpressionComparedToExpression
. Não há uma maneira documentada de fazer isso, mas se você estiver curioso, poderá ativar o sinalizador de rastreamento não documentado 9479:
Observe que a junção final produz 20.608 linhas a partir de duas entradas de linha única, mas isso não deve ser uma surpresa. É o mesmo plano produzido pelo CE original sob TF 9481.
Mencionei que os detalhes são complicados (e demorados para investigar), mas, tanto quanto posso dizer, a causa raiz do problema está relacionada ao predicado rId = 508
, com uma seletividade zero. Essa estimativa zero é aumentada para uma linha da maneira normal, o que parece contribuir para a estimativa de seletividade zero na junção em questão quando considera os predicados mais baixos na árvore de entrada (portanto, carrega as estatísticas para bId
).
Permitir que a junção externa mantenha uma estimativa do lado interno de linha zero (em vez de aumentar para uma linha) (para que todas as linhas externas se qualifiquem) fornece uma estimativa de junção "livre de erros" em qualquer calculadora. Se você estiver interessado em explorar isso, o sinalizador de rastreamento não documentado é 9473 (sozinho):
O comportamento da estimativa de cardinalidade de junção com CSelCalcExpressionComparedToExpression
também pode ser modificado para não levar em conta `` bId '' com outro sinalizador de variação não documentado (9494). Menciono tudo isso porque sei que você tem interesse em tais coisas; não porque eles oferecem uma solução. Até que você relate o problema à Microsoft e ele o resolva (ou não), expressar a consulta de maneira diferente é provavelmente o melhor caminho a seguir. Independentemente de o comportamento ser intencional ou não, eles devem estar interessados em ouvir sobre a regressão.
Finalmente, para arrumar uma outra coisa mencionada no script de reprodução: a posição final do Filtro no plano de perguntas é o resultado de uma exploração baseada em custos GbAggAfterJoinSel
movendo o agregado e o filtro acima da junção, uma vez que a saída da junção tem uma quantidade tão pequena numero de linhas. O filtro estava inicialmente abaixo da junção, conforme o esperado.