Uma consulta de procedimento armazenado às vezes obtém um plano ruim após a atualização das estatísticas em uma das tabelas, mas pode ser recompilada para o plano bom logo em seguida. Mesmos parâmetros compilados.
O problema parece vir de uma pequena tabela temporária criada no SP e depois unida. O plano inválido tem um aviso na tabela temporária de que a coluna de junção não possui estatísticas. O que da?
SQL Server 2016 SP1 CU4, com nível de compatibilidade de 2014
Plano ruim:
Bom plano:
Procedimento armazenado
USE AppDB
GO
SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
GO
CREATE PROCEDURE [MySchema].[MySP]
@MyId VARCHAR(50),
@Months INT
AS
BEGIN
SET NOCOUNT ON
SELECT *
INTO #MyTemp
FROM AppDB.MySchema.View_Feeder vf WITH (NOLOCK)
WHERE vf.MyId = @MyId AND vf.Status IS NOT NULL
SELECT wd.Col1
, vp.Col2
, vp.Col3
FROM AppDB.MySchema.View_VP vp WITH (FORCESEEK)
INNER JOIN #MyTemp wd ON wd.Col1 = vp.Col1
WHERE vp.Col3 > DATEADD(MONTH, @Months * -1, GETDATE())
END
Visão interna
USE AppDB
GO
SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
GO
CREATE VIEW [MySchema].[View_VP]
AS
SELECT pp.Col1,
pd.Col2 AS Col2,
MAX(pp.Col4) AS Col3
FROM P_DB..LargeTable pp WITH (NOLOCK)
INNER JOIN P_DB..SmallTable pd WITH (NOLOCK) ON pp.P_Id = pd.P_Id
WHERE pp.[Status] IN (3, 4)
GROUP BY pp.Col1, pd.Col2
Planos
Redigido bom plano e plano ruim .
Informação adicional
A FORCESEEK
dica foi adicionada na época para tentar lidar com esse mesmo problema e estabilizar o plano. E de qualquer forma, com ou sem isso, eu realmente gostaria de entender o que está acontecendo aqui.
Não consigo reproduzir o problema à vontade, por isso é difícil dizer se substituir SELECT INTO
por uma tabela explícita faria diferença. No entanto, acredito que deve se comportar da mesma maneira.
SELECT
database_id,
is_auto_create_stats_on,
is_auto_update_stats_on,
is_auto_update_stats_async_on
FROM sys.databases
WHERE
database_id IN (2, <relevant user databases>)
retorna:
database_id is_auto_create_stats_on is_auto_update_stats_on is_auto_update_stats_async_on
------------- ------------------------- ------------------------- -------------------------------
2 1 1 0
7 1 1 1
37 1 1 1
É claro que essa busca é terrível, mas a questão é por que ela não faz a boa busca em primeiro lugar.
A consulta não retorna 1 milhão de linhas, as estimativas estão erradas. Pode haver pequenas alterações na saída, mas o número de linhas é sempre muito baixo (talvez centenas no máximo).
Mesmo os que retornam relativamente muitas linhas geram planos buscando pelo Id
e nunca pelo status
(o que não é seletivo como você pode ver). Não consigo reproduzir o plano de busca de status, não importa quais valores sejam compilados. Eu até tentei adicionar um waitfor delay
entre a criação da tabela temporária e a segunda consulta, e atualizar estatísticas/recompilar em uma segunda sessão, sem efeito também.
Pode haver uma razão mais esotérica para isso, mas é mais provável que seja uma simples falha na criação de estatísticas. Isso pode, por exemplo, ocorrer quando a tarefa não consegue obter os recursos de memória necessários ou quando a criação de estatísticas está sendo limitada (muitas compilações simultâneas). Consulte o Microsoft White Paper Statistics Usado pelo Query Optimizer no Microsoft SQL Server 2008 . Você pode depurar isso ainda mais olhando para as estatísticas automáticas do Profiler ou Extended Events e outros eventos na mesma época.
Dito isso, muito mais informações e investigações seriam necessárias para colocar a culpa pela seleção do plano na porta das estatísticas da tabela temporária ausentes. Mesmo sem estatísticas detalhadas, o otimizador ainda pode ver a cardinalidade total da tabela temporária, e isso parece ser um fator importante aqui.
O
@Months
parâmetro pode ser o mesmo, mas o número de linhas na tabela temporária (da visão desconhecidaView_Feeder
) é diferente e os planos fornecidos não mostram o valor de@MyId
.Partindo das informações disponíveis: O plano 'bom' (somente estimativas, sem dados de desempenho apresentados) é baseado em uma tabela temporária contendo 4 linhas . O 'plano ruim' é baseado em uma tabela temporária com 114 linhas . Certamente a falta de informações de densidade e histograma pode não ser útil, mas é fácil ver como o otimizador pode escolher um plano diferente para 4 versus 114 linhas, embora com densidade e distribuição desconhecidas.
Se as estimativas dos operadores do plano não dependentes da tabela temporária estiverem muito erradas, isso é um forte sinal de que as estatísticas atuais da tabela principal não são representativas dos dados subjacentes. A falta de informação na pergunta torna isso impossível de avaliar.
No entanto, é possível ver que o otimizador está sendo solicitado a escolher entre alternativas sub-ótimas aqui. Nenhum dos planos apresentados representa uma escolha 'obviamente boa', uma vez que ambos envolvem pesquisas (falta de um índice de 'cobertura') e filtragem tardia (veja a seguir). As pesquisas em particular têm um alto custo associado a elas, que depende sensivelmente das estimativas de cardinalidade.
O uso de uma visualização restringe as opções do otimizador e das dicas:
GROUP BY
que impede que o predicadovp.Col3 > DATEADD(MONTH, @Months * -1, GETDATE())
seja empurrado para baixo, mesmo que a transformação seja válida neste caso muito específico.FORCESEEK
simplesmente pede ao otimizador para encontrar qualquer plano de busca de índice (não necessariamente usando o índice que você preferir). A remoção da visualização também removeria essa restrição.Permitir que o predicado seja empurrado para baixo também deve abrir oportunidades de indexação na tabela grande. Por exemplo:
...fornece um bom caminho de acesso para a consulta reescrita:
Outra consideração é o efeito do cache temporário de tabelas e estatísticas, conforme descrito em meus artigos Tabelas temporárias em procedimentos armazenados e Cache de tabela temporária explicados . Se um bom plano depende do conteúdo atual do objeto temporário, um explícito
UPDATE STATISTICS #MyTemp;
antes da consulta principal e adicionarOPTION (RECOMPILE)
à consulta principal pode ser uma boa solução.Como alternativa, se uma forma de plano específica for sempre ideal para essa consulta, você terá muitas opções disponíveis, incluindo uma variedade de dicas, guias de plano e imposição de plano de armazenamento de consultas. Você pode achar que usar uma variável de tabela em vez de uma tabela temporária é a melhor escolha, pois favorece o caso de baixa cardinalidade e não fornece (ou depende de) estatísticas.
Em resumo, existem várias melhorias gerais que devem ser realizadas antes de se preocupar com as razões para (do efeito de) estatísticas ausentes ocasionais na tabela temporária:
RECOMPILE
se a escolha do plano é muito sensível aos valores dos parâmetrosUPDATE STATISTICS
eRECOMPILE
se as estatísticas em cache forem um problemaSELECT INTO
fornecer informações úteis para o otimizadorNOLOCK
dicas para aumentar o desempenhoReprodução
O seguinte foi construído a partir das informações limitadas disponíveis nos planos de execução redigidos fornecidos:
A consulta é:
Sem estatísticas reais nas tabelas base, isso favorece planos próximos ao exemplo do 'plano ruim' (usando
ix_Status
):Isso sugere que a informação sobre a seletividade de
Col1
é um fator importante na escolha do otimizador.