Por que uma subconsulta reduz a estimativa de linha para 1?

26

Considere a seguinte consulta artificial, porém simples:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END AS ID2
FROM X_HEAP;

Eu esperaria que a estimativa final da linha para esta consulta fosse igual ao número de linhas na X_HEAPtabela. Tudo o que estou fazendo na subconsulta não deve importar para a estimativa de linha, pois ela não pode filtrar nenhuma linha. No entanto, no SQL Server 2016, vejo a estimativa de linha reduzida para 1 devido à subconsulta:

consulta incorreta

Por que isso acontece? O que posso fazer sobre isso?

É muito fácil reproduzir esse problema com a sintaxe correta. Aqui está um conjunto de definições de tabela que fará isso:

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL)
CREATE TABLE dbo.X_OTHER_TABLE (ID INT NOT NULL);
CREATE TABLE dbo.X_OTHER_TABLE_2 (ID INT NOT NULL);

INSERT INTO dbo.X_HEAP WITH (TABLOCK)
SELECT TOP (1000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values;

CREATE STATISTICS X_HEAP__ID ON X_HEAP (ID) WITH FULLSCAN;

link de violino db .

Joe Obbish
fonte

Respostas:

22

Esse problema de estimativa de cardinalidade (CE) surge quando:

  1. A junção é uma junção externa com uma passagem predicado de
  2. A seletividade do predicado de passagem é estimada em exatamente 1 .

Nota: A calculadora específica usada para determinar a seletividade não é importante.


Detalhes

O CE calcula a seletividade da junção externa como a soma de:

  • A junção interna seletividade de com o mesmo predicado
  • A seletividade anti-junção com o mesmo predicado

A única diferença entre uma junção externa e interna é que uma junção externa também retorna linhas que não coincidem no predicado da junção. A anti junção fornece exatamente essa diferença. A estimativa de cardinalidade para junção interna e anti-junção é mais fácil do que para junção externa diretamente.

O processo de estimativa de seletividade de junção é muito simples:

  • Primeiro, a seletividade do predicado de passagem é avaliada. SPT
    • Isso é feito usando a calculadora apropriada às circunstâncias.
    • O predicado é a coisa toda, incluindo qualquer IsFalseOrNullcomponente negativo .
  • Seletividade de junção interna: = 1 - SPT
  • Seletividade anti-junção: = SPT

A anti junção representa linhas que 'passarão' pela junção. A junção interna representa linhas que não 'passarão'. Observe que 'passagem' significa linhas que fluem através da junção sem executar o lado interno. Para enfatizar: todas as linhas serão retornadas pela junção, a distinção é entre as linhas que executam o lado interno da junção antes de emergir e as que não o fazem.

Claramente, adicionar a sempre deve fornecer uma seletividade total de 1, o que significa que todas as linhas são retornadas pela associação, conforme o esperado.1 - SPTSPT

De fato, o cálculo acima funciona exatamente como descrito para todos os valores de exceto 1 .SPT

Quando = 1, as seletividades de junção interna e anti-junção são estimadas em zero, resultando em uma estimativa de cardinalidade (para a junção como um todo) de uma linha. Tanto quanto posso dizer, isso não é intencional e deve ser relatado como um bug.SPT


Um problema relacionado

É mais provável que esse bug se manifeste do que se poderia pensar, devido a uma limitação separada do CE. Isso ocorre quando a CASEexpressão usa uma EXISTScláusula (como é comum). Por exemplo, a seguinte consulta modificada da pergunta não encontra a estimativa inesperada de cardinalidade:

-- This is fine
SELECT 
    CASE
        WHEN XH.ID = 1
        THEN (SELECT TOP (1) XOT.ID FROM dbo.X_OTHER_TABLE AS XOT) 
    END
FROM dbo.X_HEAP AS XH;

A introdução de um trivial EXISTScausa o problema:

-- This is not fine
SELECT 
    CASE
        WHEN EXISTS (SELECT 1 WHERE XH.ID = 1)
        THEN (SELECT TOP (1) XOT.ID FROM dbo.X_OTHER_TABLE AS XOT) 
    END
FROM dbo.X_HEAP AS XH;

Usar EXISTSapresenta uma semi-junção (destacada) no plano de execução:

Plano de semi-adesão

A estimativa para a semi junção é boa. O problema é que o CE trata a coluna da sonda associada como uma projeção simples, com uma seletividade fixa de 1:

Semijoin with probe column treated as a Project.

Selectivity of probe column = 1

Isso atende automaticamente a uma das condições necessárias para que esse problema da CE se manifeste, independentemente do conteúdo da EXISTScláusula.


Para informações importantes, consulte Subconsultas em CASEexpressões de Craig Freedman.

Paul White diz que a GoFundMonica
fonte
22

Definitivamente, parece um comportamento não intencional. É verdade que as estimativas de cardinalidade não precisam ser consistentes em cada etapa de um plano, mas este é um plano de consulta relativamente simples e a estimativa final de cardinalidade é inconsistente com o que a consulta está fazendo. Uma estimativa de cardinalidade tão baixa pode resultar em más escolhas para tipos de junção e métodos de acesso para outras tabelas a jusante em um plano mais complicado.

Por tentativa e erro, podemos fazer algumas consultas semelhantes para as quais o problema não aparece:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT -1) 
  END AS ID2
FROM dbo.X_HEAP;

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END AS ID2
FROM dbo.X_HEAP;

Também podemos criar mais consultas para as quais o problema aparece:

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
  END AS ID2
FROM dbo.X_HEAP;

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT -1) 
  END AS ID2
FROM dbo.X_HEAP;

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END AS ID2
FROM dbo.X_HEAP;

Parece haver um padrão: se houver uma expressão dentro da CASEqual não se espera que seja executada e a expressão do resultado seja uma subconsulta contra uma tabela, a estimativa da linha cairá para 1 após essa expressão.

Se eu escrever a consulta em uma tabela com um índice clusterizado, as regras mudarão um pouco. Podemos usar os mesmos dados:

CREATE TABLE dbo.X_CI (ID INT NOT NULL, PRIMARY KEY (ID))

INSERT INTO dbo.X_CI WITH (TABLOCK)
SELECT * FROM dbo.X_HEAP;

UPDATE STATISTICS X_CI WITH FULLSCAN;

Esta consulta tem uma estimativa final de 1000 linhas:

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
  END
FROM dbo.X_CI;

Mas esta consulta tem uma estimativa final de 1 linha:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END
FROM dbo.X_CI;

Para aprofundar isso, podemos usar o sinalizador de rastreamento não documentado 2363 para obter informações sobre como o otimizador de consulta executou cálculos de seletividade. Achei útil emparelhar esse sinalizador de rastreamento com o sinalizador de rastreamento não documentado 8606 . O TF 2363 parece fornecer cálculos de seletividade para a árvore simplificada e a árvore após a normalização do projeto. A ativação de ambos os sinalizadores de rastreamento deixa claro quais cálculos se aplicam a qual árvore.

Vamos tentar a consulta original postada na pergunta:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END AS ID2
FROM X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Aqui está parte da parte do resultado que eu acho relevante, juntamente com alguns comentários:

Plan for computation:

  CSelCalcColumnInInterval -- this is the type of calculator used

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID -- this is the column used for the calculation

Pass-through selectivity: 0 -- all rows are expected to have a true value for the case expression

Stats collection generated: 

  CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter) -- the row estimate after the join will still be 1000

      CStCollBaseTable(ID=1, CARD=1000 TBL: X_HEAP)

      CStCollBaseTable(ID=2, CARD=1 TBL: X_OTHER_TABLE)

...

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 1 -- no rows are expected to have a true value for the case expression

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1 x_jtLeftOuter) -- the row estimate after the join will still be 1

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter) -- here is the row estimate after the previous join

          CStCollBaseTable(ID=1, CARD=1000 TBL: X_HEAP)

          CStCollBaseTable(ID=2, CARD=1 TBL: X_OTHER_TABLE)

      CStCollBaseTable(ID=3, CARD=1 TBL: X_OTHER_TABLE_2)

Agora vamos tentar uma consulta semelhante que não tenha o problema. Vou usar este:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT -1) 
  END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Saída de depuração no final:

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 1

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

      CStCollConstTable(ID=4, CARD=1) -- this is different than before because we select a constant instead of from a table

Vamos tentar outra consulta para a qual a estimativa de linha incorreta está presente:

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
  END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

No final, a estimativa de cardinalidade cai para 1 linha, novamente após a seletividade de passagem = 1. A estimativa de cardinalidade é preservada após uma seletividade de 0,501 e 0,499.

Plan for computation:

 CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 0.501

...

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 0.499

...

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 1

Stats collection generated: 

  CStCollOuterJoin(ID=12, CARD=1 x_jtLeftOuter) -- this is associated with the ELSE expression

      CStCollOuterJoin(ID=11, CARD=1000 x_jtLeftOuter)

          CStCollOuterJoin(ID=10, CARD=1000 x_jtLeftOuter)

              CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)

              CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

          CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

      CStCollBaseTable(ID=4, CARD=1 TBL: X_OTHER_TABLE)

Vamos novamente mudar para outra consulta semelhante que não tem o problema. Vou usar este:

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Na saída de depuração, nunca há uma etapa que tenha uma seletividade de passagem igual a 1. A estimativa de cardinalidade permanece em 1000 linhas.

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 0.499

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

      CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

End selectivity computation

E a consulta quando ela envolve uma tabela com um índice clusterizado? Considere a seguinte consulta com o problema de estimativa de linha:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END
FROM dbo.X_CI
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

O final da saída de depuração é semelhante ao que já vimos:

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_CI].ID

Pass-through selectivity: 1

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

      CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

No entanto, a consulta no IC sem o problema tem uma saída diferente. Usando esta consulta:

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
  END
FROM dbo.X_CI
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Resultados em diferentes calculadoras sendo usadas. CSelCalcColumnInIntervalnão aparece mais:

Plan for computation:

  CSelCalcFixedFilter (0.559)

Pass-through selectivity: 0.559

Stats collection generated: 

  CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

      CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)

      CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

...

Plan for computation:

  CSelCalcUniqueKeyFilter

Pass-through selectivity: 0.001

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

      CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE)

Em conclusão, parece que obtemos uma estimativa de linha incorreta após a subconsulta nas seguintes condições:

  1. A CSelCalcColumnInIntervalcalculadora de seletividade é usada. Não sei exatamente quando isso é usado, mas parece aparecer com muito mais frequência quando a tabela base é uma pilha.

  2. Seletividade de passagem = 1. Em outras palavras, uma das CASEexpressões deve ser avaliada como falsa para todas as linhas. Não importa se a primeira CASEexpressão é avaliada como verdadeira para todas as linhas.

  3. Existe uma associação externa a CStCollBaseTable. Em outras palavras, a CASEexpressão do resultado é uma subconsulta em uma tabela. Um valor constante não funcionará.

Talvez sob essas condições, o otimizador de consulta esteja aplicando inadvertidamente a seletividade de passagem à estimativa de linha da tabela externa, em vez de ao trabalho realizado na parte interna do loop aninhado. Isso reduziria a estimativa de linha para 1.

Consegui encontrar duas soluções alternativas. Não foi possível reproduzir o problema ao usar em APPLYvez de uma subconsulta. A saída do sinalizador de rastreamento 2363 foi muito diferente APPLY. Aqui está uma maneira de reescrever a consulta original na pergunta:

SELECT 
  h.ID
, a.ID2
FROM X_HEAP h
OUTER APPLY
(
SELECT CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END
) a(ID2);

boa consulta 1

O CE herdado parece evitar o problema também.

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END AS ID2
FROM X_HEAP
OPTION (USE HINT('FORCE_LEGACY_CARDINALITY_ESTIMATION'));

boa consulta 2

Um item de conexão foi enviado para esse problema (com alguns dos detalhes que Paul White forneceu em sua resposta).

Joe Obbish
fonte