SQL Server 2014: alguma explicação para estimativa inconsistente de cardinalidade de junção automática?

27

Considere o seguinte plano de consulta no SQL Server 2014:

insira a descrição da imagem aqui

No plano de consulta, uma união automática ar.fId = ar.fIdgera uma estimativa de 1 linha. No entanto, essa é uma estimativa logicamente inconsistente: arpossui 20,608linhas e apenas um valor distinto de fId(refletido com precisão nas estatísticas). Portanto, essa junção produz o produto cruzado completo de linhas ( ~424MMlinhas), fazendo com que a consulta seja executada por várias horas.

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. Alguma ideia?

Investigação inicial e detalhes adicionais

Com base na resposta de Paul aqui , parece que as heurísticas do SQL 2012 e do SQL 2014 para estimar a cardinalidade da junção devem lidar facilmente com uma situação em que dois histogramas idênticos precisam ser comparados.

Comecei com a saída do sinalizador de rastreamento 2363, mas não conseguia entender isso facilmente. O seguinte trecho significa que o SQL Server está comparando histogramas para fIde bIdpara estimar a seletividade de uma associação que usa apenas fId? Nesse caso, isso obviamente não seria correto. Ou estou interpretando mal a saída do sinalizador de rastreamento?

Plan for computation:
  CSelCalcExpressionComparedToExpression( QCOL: [ar].fId x_cmpEq QCOL: [ar].fId )
Loaded histogram for column QCOL: [ar].bId from stats with id 3
Loaded histogram for column QCOL: [ar].fId from stats with id 1
Selectivity: 0

Observe que eu vim com várias soluções alternativas, que são incluídas no script de reprodução completa e reduzem essa consulta a milissegundos. Esta pergunta está focada em entender o comportamento, como evitá-lo em consultas futuras e determinar se é um bug que deve ser arquivado na Microsoft.

Aqui está um script completo de reprodução , aqui está a saída completa do sinalizador de rastreamento 2363 e aqui estão as definições de consulta e tabela, caso você queira examiná-las rapidamente sem abrir o script completo:

WITH cte AS (
    SELECT ar.fId, 
        ar.bId,
        MIN(CONVERT(INT, ar.isT)) AS isT,
        MAX(CONVERT(INT, tcr.isS)) AS isS
    FROM  #SQL2014MinMaxAggregateCardinalityBug_ar ar 
    LEFT OUTER JOIN #SQL2014MinMaxAggregateCardinalityBug_tcr tcr
        ON tcr.rId = 508
        AND tcr.fId = ar.fId
        AND tcr.bId = ar.bId
    GROUP BY ar.fId, ar.bId
)
SELECT s.fId, s.bId, s.isS, t.isS
FROM cte s 
JOIN cte t 
    ON t.fId = s.fId 
    AND t.isT = 1

CREATE TABLE #SQL2014MinMaxAggregateCardinalityBug_ar (
    fId INT NOT NULL,
    bId INT NOT NULL,
    isT BIT NOT NULL
    PRIMARY KEY (fId, bId)
)

CREATE TABLE #SQL2014MinMaxAggregateCardinalityBug_tcr (
    rId INT NOT NULL,
    fId INT NOT NULL,
    bId INT NOT NULL,
    isS BIT NOT NULL
    PRIMARY KEY (rId, fId, bId, isS)
)
Geoff Patterson
fonte

Respostas:

23

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 = 1pressionado abaixo dela, onde t.isTestá MIN(CONVERT(INT, ar.isT)). Apesar disso, o cálculo da seletividade para o isTpredicado pode ser usado CSelCalcColumnInIntervalem 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 CSelCalcExpressionComparedToExpressioncalculadora:

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 fIdhistogramas não filtrados , modificados pela seletividade do filtro, fornecendo 20608 * 20608 * 4.85248e-005 = 20608linhas é muito razoável.

Seguir esse cálculo significaria usar a calculadora em CSelCalcSimpleJoinWithDistinctCountsvez 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:

9479 plan

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):

9473 plan

O comportamento da estimativa de cardinalidade de junção com CSelCalcExpressionComparedToExpressiontambé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 GbAggAfterJoinSelmovendo 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.

Paul White diz que a GoFundMonica
fonte