Considere a seguinte consulta artificial, mas 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 de linha final para essa consulta fosse igual ao número de linhas na X_HEAP
tabela. O que quer que eu esteja fazendo na subconsulta não deve importar para a estimativa de linha porque ela não pode filtrar nenhuma linha. No entanto, no SQL Server 2016, vejo a estimativa de linha reduzida para 1 por causa da subconsulta:
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ão 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 do violino db .
Isso 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 esse é um plano de consulta relativamente simples e a estimativa de cardinalidade final é inconsistente com o que a consulta está fazendo. Uma estimativa de cardinalidade tão baixa pode resultar em escolhas ruins para tipos de junção e métodos de acesso para outras tabelas downstream em um plano mais complicado.
Por tentativa e erro, podemos criar algumas consultas semelhantes para as quais o problema não aparece:
Também podemos criar mais consultas para as quais o problema aparece:
Parece haver um padrão: se houver uma expressão dentro do
CASE
que não se espera que seja executada e a expressão de resultado for uma subconsulta em uma tabela, a estimativa de linha cairá para 1 após essa expressão.Se eu escrever a consulta em uma tabela com um índice clusterizado, as regras mudam um pouco. Podemos usar os mesmos dados:
Esta consulta tem uma estimativa final de 1.000 linhas:
Mas esta consulta tem uma estimativa final de 1 linha:
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 tanto para a árvore simplificada quanto para a árvore após a normalização do projeto. Ter ambos os sinalizadores de rastreamento habilitados deixa claro quais cálculos se aplicam a qual árvore.
Vamos tentar para a consulta original postada na pergunta:
Aqui está parte da saída que eu acho relevante, juntamente com alguns comentários:
Agora vamos tentar para uma consulta semelhante que não tenha o problema. Vou usar este:
Saída de depuração no final:
Vamos tentar outra consulta para a qual a estimativa de linha incorreta está presente:
No final, a estimativa de cardinalidade cai para 1 linha, novamente após Seletividade de passagem = 1. A estimativa de cardinalidade é preservada após uma seletividade de 0,501 e 0,499.
Vamos novamente mudar para outra consulta semelhante que não apresenta o problema. Vou usar este:
Na saída de depuração nunca há uma etapa que tenha uma seletividade de passagem de 1. A estimativa de cardinalidade permanece em 1.000 linhas.
E a consulta quando envolve uma tabela com um índice clusterizado? Considere a seguinte consulta com o problema de estimativa de linha:
O final da saída de depuração é semelhante ao que já vimos:
No entanto, a consulta no IC sem o problema tem uma saída diferente. Usando esta consulta:
Resultados em diferentes calculadoras sendo usadas.
CSelCalcColumnInInterval
não aparece mais:Em conclusão, parecemos obter uma estimativa de linha ruim após a subconsulta nas seguintes condições:
A
CSelCalcColumnInInterval
calculadora de seletividade é usada. Não sei exatamente quando isso é usado, mas parece aparecer com muito mais frequência quando a tabela base é um heap.Seletividade de passagem = 1. Em outras palavras,
CASE
espera-se que uma das expressões seja avaliada como falsa para todas as linhas. Não importa se a primeiraCASE
expressão for avaliada como verdadeira para todas as linhas.Existe uma junção externa para
CStCollBaseTable
. Em outras palavras, aCASE
expressão de resultado é uma subconsulta em uma tabela. Um valor constante não funcionará.Perhaps under those conditions the query optimizer is unintentionally applying the pass-through selectivity to the row estimate of the outer table instead of to the work done on the inner part of the nested loop. That would reduce the row estimate to 1.
I was able to find two workarounds. I was not able to reproduce the issue when using
APPLY
instead of a subquery. The output of trace flag 2363 was very different withAPPLY
. Here's one way to rewrite the original query in the question:The legacy CE appears to avoid the issue as well.
A connect item was submitted for this issue (with some of the details that Paul White provided in his answer).
This cardinality estimation (CE) issue surfaces when:
Note: The particular calculator used to determine the selectivity is not important.
Details
The CE computes the selectivity of the outer join as the sum of:
The only difference between an outer and inner join is that an outer join also returns rows that do not match on the join predicate. The anti join provides exactly this difference. Cardinality estimation for inner and anti join is easier than for outer join directly.
The join selectivity estimation process is very straightforward:
SPT
of the pass-through predicate is assessed.IsFalseOrNull
component.1 - SPT
SPT
The anti join represents rows that will 'pass through' the join. The inner join represents rows that will not 'pass through'. Note that 'pass through' means rows that flow through the join without running the inner side at all. To emphasise: all rows will be returned by the join, the distinction is between rows that run the inner side of the join before emerging, and those that do not.
Clearly, adding
1 - SPT
toSPT
should always give a total selectivity of 1, meaning all rows are returned by the join, as expected.Indeed, the above calculation works exactly as described for all values of
SPT
except 1.When
SPT
= 1, both inner join and anti join selectivities are estimated to be zero, resulting in a cardinality estimate (for the join as a whole) of one row. As far as I can tell, this is unintentional, and should be reported as a bug.A related issue
This bug is more likely to manifest than one might think, due to a separate CE limitation. This arises when the
CASE
expression uses anEXISTS
clause (as is common). For example the following modified query from the question does not encounter the unexpected cardinality estimate:Introducing a trivial
EXISTS
does cause the issue to surface:Using
EXISTS
introduces a semi join (highlighted) to the execution plan:The estimate for the semi join is fine. The problem is that the CE treats the associated probe column as a simple projection, with a fixed selectivity of 1:
Isso atende automaticamente a uma das condições exigidas para que esta emissão de CE se manifeste, independentemente do conteúdo da
EXISTS
cláusula.Para obter informações básicas importantes, consulte Subconsultas em
CASE
expressões de Craig Freedman.