Tenho uma query que roda em 800 milissegundos no SQL Server 2012 e leva cerca de 170 segundos no SQL Server 2014 . Acho que reduzi isso a uma estimativa de cardinalidade ruim para o Row Count Spool
operador. Eu li um pouco sobre operadores de spool (por exemplo, here e here ), mas ainda estou tendo problemas para entender algumas coisas:
- Por que essa consulta precisa de um
Row Count Spool
operador? Não acho que seja necessário para correção, então qual otimização específica ele está tentando fornecer? - Por que o SQL Server estima que a junção ao
Row Count Spool
operador remove todas as linhas? - Isso é um bug no SQL Server 2014? Se sim, vou arquivar no Connect. Mas eu gostaria de uma compreensão mais profunda primeiro.
Observação: posso reescrever a consulta como a LEFT JOIN
ou adicionar índices às tabelas para obter um desempenho aceitável no SQL Server 2012 e no SQL Server 2014. Portanto, esta questão é mais sobre entender esta consulta específica e planejar em profundidade e menos sobre como formular a consulta de maneira diferente.
A consulta lenta
Veja este Pastebin para um script de teste completo. Aqui está a consulta de teste específica que estou analisando:
-- Prune any existing customers from the set of potential new customers
-- This query is much slower than expected in SQL Server 2014
SELECT *
FROM #potentialNewCustomers -- 10K rows
WHERE cust_nbr NOT IN (
SELECT cust_nbr
FROM #existingCustomers -- 1MM rows
)
SQL Server 2014: o plano de consulta estimado
O SQL Server acredita que o Left Anti Semi Join
para o Row Count Spool
filtrará as 10.000 linhas para 1 linha. Por esse motivo, ele seleciona um LOOP JOIN
para a junção subsequente a #existingCustomers
.
SQL Server 2014: o plano de consulta real
Como esperado (por todos menos o SQL Server!), o Row Count Spool
não removeu nenhuma linha. Portanto, estamos fazendo um loop de 10.000 vezes quando o SQL Server esperava fazer um loop apenas uma vez.
SQL Server 2012: o plano de consulta estimado
Ao usar o SQL Server 2012 (ou OPTION (QUERYTRACEON 9481)
no SQL Server 2014), o Row Count Spool
não reduz o número estimado de linhas e uma junção de hash é escolhida, resultando em um plano muito melhor.
A reescrita LEFT JOIN
Para referência, aqui está uma maneira de reescrever a consulta para obter um bom desempenho em todos os SQL Server 2012, 2014 e 2016. No entanto, ainda estou interessado no comportamento específico da consulta acima e se ela é um bug no novo Estimador de cardinalidade do SQL Server 2014.
-- Re-writing with LEFT JOIN yields much better performance in 2012/2014/2016
SELECT n.*
FROM #potentialNewCustomers n
LEFT JOIN (SELECT 1 AS test, cust_nbr FROM #existingCustomers) c
ON c.cust_nbr = n.cust_nbr
WHERE c.test IS NULL
A
cust_nbr
coluna em#existingCustomers
é anulável. Se realmente contiver nulos, a resposta correta aqui é retornar zero linhas (NOT IN (NULL,...)
sempre produzirá um conjunto de resultados vazio).Portanto, a consulta pode ser considerada como
Com o spool de contagem de linhas lá para evitar ter que avaliar o
Mais de uma vez.
Este parece ser apenas um caso em que uma pequena diferença nas suposições pode fazer uma diferença catastrófica no desempenho.
Depois de atualizar uma única linha como abaixo...
... a consulta foi concluída em menos de um segundo. As contagens de linhas nas versões reais e estimadas do plano agora estão quase corretas.
Linhas zero são geradas conforme descrito acima.
Os histogramas de estatísticas e os limites de atualização automática no SQL Server não são granulares o suficiente para detectar esse tipo de alteração de linha única. Indiscutivelmente, se a coluna for anulável, pode ser razoável trabalhar com base no fato de que contém pelo menos um
NULL
, mesmo que o histograma de estatísticas não indique atualmente que haja algum.Veja a resposta completa de Martin para esta pergunta. O ponto-chave é que, se uma única linha dentro de
NOT IN
forNULL
, a lógica booleana funcionará de forma que "a resposta correta seja retornar zero linhas". ORow Count Spool
operador está otimizando essa lógica (necessária).A Microsoft fornece um excelente white paper sobre o SQL 2014 Cardinality Estimator . Neste documento, encontrei as seguintes informações:
O novo CE assume que os valores consultados existem no conjunto de dados, mesmo que o valor esteja fora do intervalo do histograma. O novo CE neste exemplo usa uma frequência média que é calculada multiplicando a cardinalidade da tabela pela densidade.
Freqüentemente, essa mudança é muito boa; ele alivia muito o problema da chave ascendente e geralmente produz um plano de consulta mais conservador (estimativa de linha mais alta) para valores que estão fora do intervalo com base no histograma de estatísticas.
No entanto, neste caso específico, assumir que um
NULL
valor será encontrado leva à suposição de que a junção aRow Count Spool
irá filtrar todas as linhas de#potentialNewCustomers
. No caso em que de fato existe umaNULL
linha, essa é uma estimativa correta (conforme visto na resposta de Martin). No entanto, no caso de não haver umaNULL
linha, o efeito pode ser devastador porque o SQL Server produz uma estimativa pós-junção de 1 linha, independentemente de quantas linhas de entrada aparecem. Isso pode levar a opções de junção muito ruins no restante do plano de consulta.Acho que está na área cinzenta entre um bug e uma suposição ou limitação de impacto no desempenho do novo Estimador de cardinalidade do SQL Server. No entanto, essa peculiaridade pode causar regressões substanciais no desempenho em relação ao SQL 2012 no caso específico de uma
NOT IN
cláusula anulável que não possui nenhumNULL
valor.Portanto, registrei um problema de conexão para que a equipe SQL esteja ciente das possíveis implicações dessa alteração no Estimador de cardinalidade.
Atualização: Estamos no CTP3 agora para SQL16, e confirmei que o problema não ocorre lá.
A resposta de Martin Smith e sua auto-resposta abordaram todos os pontos principais corretamente, só quero enfatizar uma área para futuros leitores:
O objetivo declarado da consulta é:
Esse requisito é fácil de expressar em SQL, de várias maneiras. Qual deles é escolhido é tanto uma questão de estilo quanto qualquer outra coisa, mas a especificação da consulta ainda deve ser escrita para retornar resultados corretos em todos os casos. Isso inclui a contabilização de nulos.
Expressando completamente o requisito lógico:
Podemos então escrever uma consulta que corresponda a esses requisitos usando qualquer sintaxe que preferirmos. Por exemplo:
Isso produz um plano de execução eficiente, que retorna resultados corretos:
Podemos expressar
NOT IN
como<> ALL
ouNOT = ANY
sem afetar o plano ou resultados:Ou usando
NOT EXISTS
:Não há nada de mágico nisso, ou algo particularmente censurável em usar
IN
,ANY
, ouALL
- só precisamos escrever a consulta corretamente, para que ela sempre produza os resultados corretos.A forma mais compacta usa
EXCEPT
:Isso também produz resultados corretos, embora o plano de execução possa ser menos eficiente devido à ausência de filtragem de bitmap:
A pergunta original é interessante porque expõe um problema que afeta o desempenho com a implementação de verificação nula necessária. O objetivo dessa resposta é que escrever a consulta corretamente também evita o problema.