Sniffing de parâmetros vs VARIÁVEIS vs Recompilar vs OTIMIZAR PARA DESCONHECIDO


40

Portanto, tivemos um processo de longa duração causando problemas esta manhã (30 segundos + tempo de execução). Decidimos verificar se o cheiro do parâmetro era o culpado. Então, reescrevemos o proc e definimos os parâmetros recebidos como variáveis, a fim de impedir o sniffing de parâmetros. Uma abordagem testada / verdadeira. Bom, o tempo de consulta foi aprimorado (menos de 1 segundo). Ao examinar o plano de consulta, as melhorias foram encontradas em um índice que o original não estava usando.

Apenas para verificar se não obtivemos um falso positivo, fizemos um dccc freeproccache no proc original e reexaminamos para verificar se os resultados aprimorados seriam os mesmos. Mas, para nossa surpresa, o processo original ainda estava lento. Tentamos novamente com um WITH RECOMPILE, ainda lento (tentamos uma recompilação na chamada para o proc e dentro do próprio proc). Até reiniciamos o servidor (caixa de desenvolvimento obviamente).

Então, minha pergunta é esta ... como culpar o parâmetro sniffing quando temos a mesma consulta lenta em um cache de plano vazio ... não deve haver parâmetros para snif ???

Em vez disso, estamos sendo afetados por estatísticas da tabela não relacionadas ao cache do plano. E se sim, por que definir os parâmetros recebidos para variáveis ​​ajudaria?

Em testes adicionais, também descobrimos que a inserção de OPTION (OPTIMIZE FOR UNKNOWN) nas partes internas do proc DID obtém o plano aprimorado esperado.

Então, alguns de vocês, mais espertos que eu, podem dar algumas pistas sobre o que está acontecendo nos bastidores para produzir esse tipo de resultado?

Em outra nota, o plano lento também é abortado cedo com a razão, GoodEnoughPlanFoundenquanto o plano rápido não tem motivo de abortamento antecipado no plano real.

Em suma

  • Criando variáveis ​​a partir dos parâmetros recebidos (1 s)
  • com recompilar (mais de 30 segundos)
  • dbcc freeproccache (mais de 30 segundos)
  • OPÇÃO (OTIMIZAR PARA UKNOWN) (1 seg)

ATUALIZAR:

Consulte o plano de execução lenta aqui: https://www.dropbox.com/s/cmx2lrsea8q8mr6/plan_slow.xml

Consulte o plano de execução rápida aqui: https://www.dropbox.com/s/b28x6a01w7dxsed/plan_fast.xml

Nota: os nomes de tabela, esquema e objeto foram alterados por razões de segurança.

Respostas:


43

A consulta é

SELECT SUM(Amount) AS SummaryTotal
FROM   PDetail WITH(NOLOCK)
WHERE  ClientID = @merchid
       AND PostedDate BETWEEN @datebegin AND @dateend 

A tabela contém 103.129.000 linhas.

O plano rápido procura pelo ClientId com um predicado residual na data, mas precisa fazer 96 pesquisas para recuperar o Amount. A <ParameterList>seção no plano é a seguinte.

        <ParameterList>
          <ColumnReference Column="@dateend" 
                           ParameterRuntimeValue="'2013-02-01 23:59:00.000'" />
          <ColumnReference Column="@datebegin" 
                           ParameterRuntimeValue="'2013-01-01 00:00:00.000'" />
          <ColumnReference Column="@merchid" 
                           ParameterRuntimeValue="(78155)" />
        </ParameterList>

O plano lento pesquisa por data e possui pesquisas para avaliar o predicado residual no ClientId e recuperar a quantia (Estimado 1 versus Real 7.388.383). A <ParameterList>seção é

        <ParameterList>
          <ColumnReference Column="@EndDate" 
                           ParameterCompiledValue="'2013-02-01 23:59:00.000'" 
                           ParameterRuntimeValue="'2013-02-01 23:59:00.000'" />
          <ColumnReference Column="@BeginDate" 
                           ParameterCompiledValue="'2013-01-01 00:00:00.000'"               
                           ParameterRuntimeValue="'2013-01-01 00:00:00.000'" />
          <ColumnReference Column="@ClientID" 
                           ParameterCompiledValue="(78155)" 
                           ParameterRuntimeValue="(78155)" />
        </ParameterList>

Neste segundo caso, o nãoParameterCompiledValue está vazio. O SQL Server detectou com êxito os valores usados ​​na consulta.

O livro "Solução de problemas práticos do SQL Server 2005" tem a dizer sobre o uso de variáveis ​​locais

Usar variáveis ​​locais para impedir o sniffing de parâmetros é um truque bastante comum, mas as dicas OPTION (RECOMPILE)e OPTION (OPTIMIZE FOR)... são geralmente mais elegantes e soluções um pouco menos arriscadas


Nota

No SQL Server 2005, a compilação no nível da instrução permite que a compilação de uma instrução individual em um procedimento armazenado seja adiada até pouco antes da primeira execução da consulta. Até então, o valor da variável local seria conhecido. Teoricamente, o SQL Server poderia tirar proveito disso para detectar valores de variáveis ​​locais da mesma maneira que detecta parâmetros. No entanto, como era comum o uso de variáveis ​​locais para impedir o sniffing de parâmetro no SQL Server 7.0 e SQL Server 2000+, o sniffing de variáveis ​​locais não estava habilitado no SQL Server 2005. Ele pode ser habilitado em uma versão futura do SQL Server, o que é uma boa opção. motivo para usar uma das outras opções descritas neste capítulo, se você tiver uma escolha.


De um teste rápido para esse fim, o comportamento descrito acima ainda é o mesmo em 2008 e 2012 e as variáveis ​​não são detectadas para compilação adiada, mas apenas quando uma OPTION RECOMPILEdica explícita é usada.

DECLARE @N INT = 0

CREATE TABLE #T ( I INT );

/*Reference to #T means this statement is subject to deferred compile*/
SELECT *
FROM   master..spt_values
WHERE  number = @N
       AND EXISTS(SELECT COUNT(*) FROM #T)

SELECT *
FROM   master..spt_values
WHERE  number = @N
OPTION (RECOMPILE)

DROP TABLE #T 

Apesar da compilação adiada, a variável não é detectada e a contagem de linhas estimada é imprecisa

Estimativas vs Real

Portanto, suponho que o plano lento esteja relacionado a uma versão parametrizada da consulta.

O ParameterCompiledValueé igual a ParameterRuntimeValuepara todos os parâmetros, portanto, este não é um sniffing típico de parâmetros (onde o plano foi compilado para um conjunto de valores e depois executado para outro conjunto de valores).

O problema é que o plano que é compilado para os valores corretos dos parâmetros é inadequado.

Você provavelmente está enfrentando o problema com datas ascendentes descritas aqui e aqui . Para uma tabela com 100 milhões de linhas, você precisa inserir (ou modificar) 20 milhões antes que o SQL Server atualize automaticamente as estatísticas para você. Parece que da última vez em que foram atualizadas, zero linhas correspondiam ao período da consulta, mas agora 7 milhões o fazem.

Você pode agendar atualizações de estatísticas mais frequentes, considerar sinalizadores de rastreio 2389 - 90ou usá- OPTIMIZE FOR UKNOWNlo, para que apenas recorra a suposições, em vez de poder usar as estatísticas atualmente enganosas na datetimecoluna.

Isso pode não ser necessário na próxima versão do SQL Server (após 2012). Um item do Connect relacionado contém a resposta intrigante

Publicado pela Microsoft em 28/08/2012 às 13:35
Fizemos um aprimoramento na estimativa de cardinalidade para o próximo grande lançamento que basicamente corrige isso. Fique atento aos detalhes quando as nossas pré-visualizações forem publicadas. Eric

Esta melhoria de 2014 é analisada por Benjamin Nevarez no final do artigo:

Uma primeira olhada no novo estimador de cardinalidade do SQL Server .

Parece que o novo estimador de cardinalidade voltará e usará a densidade média nesse caso, em vez de fornecer a estimativa de 1 linha.

Alguns detalhes adicionais sobre o estimador de cardinalidade de 2014 e o principal problema crescente aqui:

Nova funcionalidade no SQL Server 2014 - Parte 2 - Nova estimativa de cardinalidade


29

Então, minha pergunta é a seguinte ... como culpar o parâmetro sniffing quando obtemos a mesma consulta lenta em um cache de plano vazio ... não deve haver parâmetros para sniffing?

Quando o SQL Server compila uma consulta contendo valores de parâmetros, ele fareja os valores específicos desses parâmetros para estimativa de cardinalidade (contagem de linhas). No seu caso, os valores particulares de @BeginDate, @EndDatee @ClientIDsão utilizados na escolha de um plano de execução. Você pode encontrar mais detalhes sobre a detecção de parâmetros aqui e aqui . Estou fornecendo esses links de segundo plano, porque a pergunta acima me faz pensar que o conceito é imperfeito no momento - sempre há valores de parâmetros a serem detectados quando um plano é compilado.

De qualquer forma, isso não vem ao caso, porque a detecção de parâmetros não é o problema aqui, como Martin Smith apontou. No momento em que a consulta lenta foi compilada, as estatísticas indicaram que não havia linhas para os valores detectados @BeginDatee @EndDate:

Valores lentos do plano aspirado

Os valores aspirados são muito recentes, sugerindo o principal problema crescente que Martin menciona. Como se estima que a busca de índice nas datas retorne apenas uma única linha, o otimizador escolhe um plano que envia o predicado ClientIDao operador Key Lookup como um resíduo.

A estimativa de linha única também é o motivo pelo qual o otimizador para de procurar planos melhores, retornando uma mensagem de plano suficiente encontrado. O custo total estimado do plano lento com a estimativa de linha única é de apenas 0,013136 unidades de custo, portanto, não faz sentido tentar encontrar algo melhor. Exceto, é claro, que a busca realmente retorna 7.388.383 linhas em vez de uma, causando o mesmo número de pesquisas de chave.

As estatísticas podem ser difíceis de manter atualizadas e úteis em tabelas grandes e o particionamento apresenta desafios próprios nesse sentido. Eu não tive sucesso em particular com os sinalizadores de rastreamento 2389 e 2390, mas você pode testá-los. As compilações mais recentes do SQL Server (R2 SP1 e posterior) têm atualizações estatísticas dinâmicas disponíveis, mas essas atualizações estatísticas por partição ainda não foram implementadas. Enquanto isso, você pode agendar uma atualização manual das estatísticas sempre que fizer alterações significativas nesta tabela.

Para essa consulta específica, eu pensaria em implementar o índice sugerido pelo otimizador durante a compilação do plano de consulta rápida:

/*
The Query Processor estimates that implementing the following index could improve
the query cost by 98.8091%.

WARNING: This is only an estimate, and the Query Processor is making this 
recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide impact,
including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index>]
ON [dbo].[PDetail] ([ClientID],[PostedDate])
INCLUDE ([Amount]);

O índice deve estar alinhado à partição, com uma ON PartitionSchemeName (PostedDate)cláusula, mas o ponto é que fornecer um caminho de acesso a dados obviamente melhor ajudará o otimizador a evitar escolhas de planos inadequadas, sem recorrer a OPTIMIZE FOR UNKNOWNdicas ou soluções alternativas à moda antiga, como o uso de variáveis ​​locais.

Com o índice aprimorado, a Pesquisa de Chave para recuperar a Amountcoluna será eliminada, o processador de consultas ainda poderá executar a eliminação dinâmica da partição e usar uma busca para encontrar o ClientIDintervalo específico e o período.


Gostaria de poder marcar duas respostas como corretas, mas novamente, obrigado pelas informações adicionais - muito instrutivas.
RThomas

11
Faz alguns anos desde que publiquei isso ... mas eu só queria que você soubesse. Eu ainda uso o termo "entendido imperfeitamente" o tempo todo, e sempre penso em Paul White quando o faço. Me faz rir sempre.
RThomas

0

Eu tive exatamente o mesmo problema em que um procedimento armazenado ficou lento OPTIMIZE FOR UNKNOWNe as RECOMPILEdicas de consulta resolveram a lentidão e aceleraram o tempo de execução. No entanto, os dois métodos a seguir não afetaram a lentidão do procedimento armazenado: (i) Limpando o cache (ii) usando WITH RECOMPILE. Assim, como você disse, não era realmente um farejador de parâmetros.

Os sinalizadores de rastreamento 2389 e 2390 também não ajudaram. Apenas atualizar as estatísticas ( EXEC sp_updatestats) fez isso por mim.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.