No dia 27 de junho, o PASS Performance Virtual Chapter realizou seu Summer Performance Palooza 2013 – uma espécie de 24 horas de PASS reduzida, mas focada apenas em tópicos relacionados ao desempenho. Dei uma sessão intitulada "10 Maus Hábitos que Podem Matar o Desempenho", tratando dos 10 conceitos a seguir:
- SELECIONAR *
- Índices cegos
- Sem prefixo de esquema
- Opções de cursor padrão
- prefixo sp_
- Permitir aumento do cache
- Tipos de dados amplos
- Padrões do SQL Server
- Uso excessivo de funções
- "Funciona na minha máquina"
Você pode se lembrar de alguns desses tópicos de apresentações como minha palestra "Maus Hábitos e Melhores Práticas" ou nossos webinars semanais de Query Tuning que venho realizando com Kevin Kline desde o início de junho até esta semana. (Esses 6 vídeos, por sinal, estarão disponíveis no início de agosto no YouTube.)
Minha sessão teve 351 participantes e obtive ótimos comentários. Eu queria abordar um pouco disso.
Primeiro, um problema de configuração:eu estava usando um microfone novinho em folha e não fazia ideia de que cada toque de tecla soaria como um trovão. Abordei esse problema com um melhor posicionamento dos meus periféricos, mas quero pedir desculpas a todos os afetados por isso.
Em seguida, os downloads; o deck e as amostras são postados no site do evento. Eles estão na parte inferior da página, mas você também pode baixá-los aqui.
Por fim, o que se segue é uma lista de perguntas que foram postadas durante a sessão, e eu queria ter certeza de que abordei todas as que não foram respondidas durante as perguntas e respostas ao vivo. Peço desculpas por ter feito isso em pouco menos de um mês , mas foram muitas perguntas, e eu não queria publicá-las em partes.
P:Se você tem um proc que pode ter valores de entrada muito variados para os parâmetros fornecidos e o resultado é que o plano em cache não é o ideal para a maioria dos casos, é melhor criar o proc WITH RECOMPILE e pegar o pequeno desempenho atingido cada vez que é executado?
R: Você terá que abordar isso caso a caso, pois realmente dependerá de vários fatores (incluindo a complexidade do plano). Observe também que você pode fazer a recompilação em nível de instrução de modo que apenas as instruções afetadas sejam atingidas, em oposição ao módulo inteiro. Paul White me lembrou que as pessoas geralmente 'consertam' o sniffing de parâmetros com
RECOMPILE
, mas muitas vezes isso significa WITH RECOMPILE
estilo 2000 em vez do muito melhor OPTION (RECOMPILE)
, que não apenas se limita à instrução, mas também permite a incorporação de parâmetros, que WITH RECOMPILE
não. Então, se você for usar RECOMPILE
para impedir a detecção de parâmetros, adicione-o à instrução, não ao módulo. P:Se você usar a opção recompilar no sql dinâmico, verá um grande impacto no desempenho
R: Como acima, isso dependerá do custo e da complexidade dos planos e não há como dizer:"Sim, sempre haverá um grande impacto no desempenho". Você também tem que ter certeza de comparar isso com a alternativa.
P:Se houver índice clusterizado em insertdate, mais tarde, quando recuperamos dados, usamos a função convert, se estiver usando comparação direta, a data da consulta não é legível, no mundo real, qual é a melhor escolha
R: Não tenho certeza do que significa "legível no mundo real". Se você quer dizer que deseja a saída em um formato específico, geralmente é melhor converter para uma string no lado do cliente. C# e a maioria das outras linguagens que você provavelmente está usando na camada de apresentação são mais do que capazes de formatar a saída de data/hora do banco de dados em qualquer formato regional desejado.
P:Como determinar o número de vezes que um plano armazenado em cache é usado – existe uma coluna com esse valor ou alguma consulta na Internet que forneça esse valor? Por fim, essas contagens só seriam relevantes desde a última reinicialização?
R: A maioria dos DMVs são válidos apenas desde o último início do serviço, e até mesmo outros podem ser liberados com mais frequência (mesmo sob demanda – tanto inadvertidamente quanto propositalmente). O cache do plano está, é claro, em fluxo constante, e os planos AFAIK que saem do cache não mantêm sua contagem anterior se voltarem. Portanto, mesmo quando você vê um plano no cache, não estou 100% confiante que você pode acreditar na contagem de uso que você encontra.
Dito isso, o que você provavelmente está procurando é
sys.dm_exec_cached_plans.usecounts
e você também pode encontrar sys.dm_exec_procedure_stats.execution_count
para ajudar a complementar as informações para procedimentos em que instruções individuais dentro dos procedimentos não são encontradas no cache. P:Quais são os problemas ao atualizar o mecanismo db para uma nova versão, mas deixar os bancos de dados do usuário em modos de compatibilidade mais antigos?
R: As principais preocupações em torno disso são a capacidade de usar certa sintaxe, como
OUTER APPLY
ou variáveis com uma função com valor de tabela. Não estou ciente de nenhum caso em que o uso de uma compatibilidade mais baixa tenha algum impacto direto no desempenho, mas algumas coisas normalmente recomendadas são reconstruir índices e atualizar estatísticas (e fazer com que seu fornecedor ofereça suporte ao nível de compatibilidade mais recente o mais rápido possível). Eu vi isso resolver a degradação inesperada do desempenho em um número substancial de casos, mas também ouvi algumas opiniões de que isso não é necessário e talvez até imprudente. P:No *, isso importa ao fazer uma cláusula exist
R: Não, pelo menos em termos de desempenho, uma exceção onde
SELECT *
não importa é quando usado dentro de um EXISTS
cláusula. Mas por que você usaria *
aqui? Eu prefiro usar EXISTS (SELECT 1 ...
– o otimizador irá tratá-los da mesma forma, mas de certa forma ele autodocumenta o código e garante que os leitores entendam que a subconsulta não retorna nenhum dado (mesmo se eles perderem o grande EXISTS
fora). Algumas pessoas usam NULL
, e não tenho ideia de por que comecei a usar 1, mas acho NULL
também pouco intuitivo. *Observação* você precisará ter cuidado, se tentar usar
EXISTS (SELECT *
dentro de um módulo vinculado ao esquema:CREATE VIEW dbo.ThisWillNotWork WITH SCHEMABINDING AS SELECT BusinessEntityID FROM Person.Person AS p WHERE EXISTS (SELECT * FROM Sales.SalesOrderHeader AS h WHERE h.SalesPersonID = p.BusinessEntityID);
Você recebe este erro:
Msg 1054, Level 15, State 6, Procedure ThisWillNotWork, Line 6
A sintaxe '*' não é permitida em objetos vinculados ao esquema.
No entanto, alterando-o para
SELECT 1
funciona muito bem. Então talvez esse seja outro argumento para evitar SELECT *
mesmo nesse cenário. P:Existe algum link de recurso para os melhores padrões de codificação?
R: Provavelmente existem centenas em uma variedade de idiomas. Assim como as convenções de nomenclatura, os padrões de codificação são uma coisa muito subjetiva. Realmente não importa qual convenção você decida funcionar melhor para você; se você gosta de
tbl
prefixos, enlouqueça! Prefira Pascal sobre bigEndian, faça isso. Deseja prefixar seus nomes de coluna com o tipo de dados, como intCustomerID
, eu não vou impedi-lo. O mais importante é que você defina uma convenção e a empregue *de forma consistente.* Dito isto, se você quer minhas opiniões, eu não tenho falta delas.
P:XACT_ABORT é algo que pode ser usado no SQL Server 2008 em diante?
R: Não conheço nenhum plano para suspender o uso de
XACT_ABORT
então deve continuar funcionando normalmente. Francamente, não vejo isso sendo usado com muita frequência agora que temos TRY / CATCH
(e THROW
a partir do SQL Server 2012). P:Como uma função de tabela embutida em uma aplicação cruzada se compara à função escalar chamada 1.000x?
R: Eu não testei isso, mas em muitos casos, substituir uma função escalar por uma função com valor de tabela embutido pode ter um grande impacto no desempenho. O problema que encontro é que fazer essa troca pode ser uma quantidade substancial de trabalho no sistema que foi escrito antes de
APPLY
existiram ou ainda são gerenciados por pessoas que não adotaram essa abordagem melhor. P:Eu tenho uma consulta que é muito lenta na primeira vez (~ 1 minuto) e rápida (~ 3 segundos) em todas as outras vezes. Por onde devo começar a ver de onde vem o problema de desempenho pela primeira vez?
R: Duas coisas vêm à mente:(1) o atraso tem a ver com o tempo de compilação ou (2) o atraso tem a ver com a quantidade de dados que está sendo carregado para satisfazer a consulta, e a primeira vez que deve vir do disco e não memória. Para (1) você pode executar a consulta no SQL Sentry Plan Explorer e a barra de status mostrará o tempo de compilação para a primeira e as invocações subsequentes (embora um minuto pareça bastante excessivo para isso e improvável). Se você não encontrar nenhuma diferença, pode ser apenas a natureza do sistema:memória insuficiente para suportar a quantidade de dados que você está tentando carregar com essa consulta em combinação com outros dados que já estavam no buffer pool. Se você não acredita que nenhum deles seja o problema, veja se as duas execuções diferentes realmente geram planos diferentes - se houver alguma diferença, poste os planos em answers.sqlperformance.com e teremos prazer em dar uma olhada . Na verdade, a captura de planos reais para ambas as execuções usando o Plan Explorer em qualquer caso também pode informar sobre quaisquer diferenças na E/S e pode levar a onde o SQL Server está gastando seu tempo na primeira execução mais lenta.
P:Estou recebendo a detecção de parâmetros usando sp_executesql. O Optimize para cargas de trabalho ad hoc resolveria isso, pois apenas o stub do plano está no cache?
R: Não, não acho que a configuração Otimizar para cargas de trabalho ad hoc ajude nesse cenário, pois a detecção de parâmetros implica que execuções subsequentes do mesmo plano sejam usadas para parâmetros diferentes e com comportamentos de desempenho significativamente diferentes. Otimizar para cargas de trabalho ad hoc é usado para minimizar o impacto drástico no cache do plano que pode ocorrer quando você tem um grande número de instruções SQL diferentes. Portanto, a menos que você esteja falando sobre o impacto no cache do plano de muitas instruções diferentes, você está enviando para
sp_executesql
– o que não seria caracterizado como sniffing de parâmetros – acho que experimentando com OPTION (RECOMPILE)
pode ter um resultado melhor ou, se você conhece os valores dos parâmetros que *fazem* produzir bons resultados em uma variedade de combinações de parâmetros, use OPTIMIZE FOR
. Esta resposta de Paul White pode fornecer uma visão muito melhor. P:Existe uma maneira de executar SQL dinâmico e NÃO salvar o plano de consulta?
R: Claro, basta incluir
OPTION (RECOMPILE)
no texto SQL dinâmico:DBCC FREEPROCCACHE; USE AdventureWorks2012; GO SET NOCOUNT ON; GO EXEC sp_executesql N'SELECT TOP (1) * INTO #x FROM Sales.SalesOrderHeader;'; GO EXEC sp_executesql N'SELECT TOP (1) * INTO #x FROM Sales.SalesOrderDetail OPTION (RECOMPILE);' GO SELECT t.[text], p.usecounts FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.[plan_handle]) AS t WHERE t.[text] LIKE N'%Sales.' + 'SalesOrder%';
Resultados:1 linha mostrando o
Sales.SalesOrderHeader
inquerir. Agora, se qualquer instrução no lote NÃO incluir
OPTION (RECOMPILE)
, o plano ainda pode ser armazenado em cache, mas não pode ser reutilizado. P:Você pode usar BETWEEN no exemplo de data de #9 se>=e
R: Bem,
BETWEEN
não é semanticamente equivalente a >= AND <
, mas sim >= AND <=
, e otimiza e executa exatamente da mesma maneira. De qualquer forma, eu propositalmente não uso BETWEEN
em consultas de intervalo de datas – sempre – porque não há como torná-lo um intervalo aberto. Com BETWEEN
, ambas as extremidades são inclusivas e isso pode ser muito problemático dependendo do tipo de dados subjacente (agora ou devido a alguma alteração futura que você talvez não conheça). O título pode parecer um pouco duro, mas eu entro em detalhes sobre isso no seguinte post do blog:O que BETWEEN e o diabo têm em comum?
P:Em um cursor, o que o "local fast_forward" realmente faz?
R:
FAST_FORWARD
é na verdade a forma abreviada de READ_ONLY
e FORWARD_ONLY
. Aqui está o que eles fazem:LOCAL
faz com que os escopos externos (por padrão um cursor sejaGLOBAL
a menos que você tenha alterado a opção de nível de instância).READ_ONLY
faz com que você não possa atualizar o cursor diretamente, por exemplo usandoWHERE CURRENT OF
.FORWARD_ONLY
impede a capacidade de rolar, por exemplo usandoFETCH PRIOR
ouFETCH ABSOLUTE
em vez deFETCH NEXT
.
Definir essas opções, como demonstrei (e escrevi sobre isso), pode ter um impacto significativo no desempenho. Muito raramente vejo cursores em produção que realmente precisam se desviar desse conjunto de recursos, mas geralmente eles são escritos para aceitar os padrões muito mais caros de qualquer maneira.
P:o que é mais eficiente, um cursor ou um loop while?
R: Um
WHILE
loop provavelmente será mais eficiente do que um cursor equivalente com as opções padrão, mas suspeito que você encontrará pouca ou nenhuma diferença se usar LOCAL FAST_FORWARD
. De um modo geral, um WHILE
loop *é* um cursor sem ser chamado de cursor, e eu desafiei alguns colegas altamente estimados a provar que eu estava errado no ano passado. Seu WHILE
loops não se saíram tão bem. P:Você não recomenda o prefixo usp para procedimentos armazenados do usuário, isso tem o mesmo impacto negativo?
R: Um
usp_
prefixo (ou qualquer prefixo diferente de sp_
, ou nenhum prefixo para esse assunto) *não* tem o mesmo impacto que demonstrei. No entanto, acho pouco valor em usar um prefixo em procedimentos armazenados porque raramente há dúvida de que, quando encontro um código que diz EXEC something
, que algo é um procedimento armazenado - portanto, há pouco valor lá (diferente, digamos, de prefixar visualizações para distingui-las de tabelas, pois elas podem ser usadas de forma intercambiável). Dar a cada procedimento o mesmo prefixo também torna muito mais difícil encontrar o objeto que você está procurando, digamos, Explorador de Objetos. Imagine se cada sobrenome na lista telefônica tivesse o prefixo LastName_
– de que forma isso te ajuda? P:Existe uma maneira de limpar planos em cache onde há várias cópias?
R: Sim! Bem, se você estiver no SQL Server 2008 ou superior. Depois de identificar dois planos idênticos, eles ainda terão
plan_handle
separados valores. Então, identifique aquele que você *não* deseja manter, copie seu plan_handle
, e coloque-o dentro deste DBCC
comando:DBCC FREEPROCCACHE(0x06.....);P:O uso de if else etc em um proc causa planos ruins, ele é otimizado para a primeira execução e otimizado apenas para esse caminho? Então, as seções de código em cada IF precisam ser feitas em procedimentos separados?
R: Como o SQL Server agora pode realizar a otimização em nível de instrução, isso tem um efeito menos drástico hoje do que em versões mais antigas, nas quais todo o procedimento precisava ser recompilado como uma unidade.
P:Às vezes, descobri que escrever sql dinâmico pode ser melhor porque elimina o problema de sniffing de parâmetros para sp. Isso é verdade ? Existem compensações ou outras considerações a serem feitas sobre esse cenário?
R: Sim, o SQL dinâmico muitas vezes pode impedir a detecção de parâmetros, particularmente no caso em que uma consulta enorme de "pia da cozinha" tem muitos parâmetros opcionais. Tratei algumas outras considerações nas perguntas acima.
P:Se eu tivesse uma coluna calculada na minha tabela como DATEPART(mycolumn, year) e no índice dela, o SQL Server usaria isso com um SEEK?
R: Deveria, mas é claro que depende da consulta. O índice pode não ser adequado para cobrir as colunas de saída ou satisfazer outros filtros e o parâmetro usado pode não ser seletivo o suficiente para justificar uma busca.
P:um plano é gerado para TODAS as consultas? É gerado um plano mesmo para os triviais?
R: Até onde eu sei, um plano é gerado para cada consulta válida, mesmo planos triviais, a menos que haja um erro que impeça a geração de um plano (isso pode acontecer em vários cenários, como dicas inválidas). Se eles são armazenados em cache ou não (e quanto tempo permanecem no cache) depende de vários outros fatores, alguns dos quais discuti acima.
P:Uma chamada para sp_executesql gera (e reutiliza) o plano em cache?
R: Sim, se você enviar exatamente o mesmo texto de consulta, não importa se você o emitir diretamente ou enviá-lo por meio de
sp_executesql
, o SQL Server armazenará em cache e reutilizará o plano. P:Tudo bem aplicar uma regra (para um ambiente de desenvolvimento) em que todas as máquinas de desenvolvimento usam inicialização instantânea de arquivo?
R: Não vejo porque não. A única preocupação que eu teria é que, com a inicialização instantânea de arquivos, os desenvolvedores podem não notar um grande número de eventos de crescimento automático, o que pode refletir configurações de crescimento automático ruins que podem ter um impacto muito diferente no ambiente de produção (especialmente se algum desses servidores *não * tem IFI habilitado).
P:Com a função na cláusula SELECT, seria correto dizer então que é melhor duplicar o código?
R: Pessoalmente, eu diria que sim. Obtive muito desempenho ao substituir funções escalares no
SELECT
list com um equivalente embutido, mesmo nos casos em que tenho que repetir esse código. Como mencionado acima, no entanto, você pode descobrir em alguns casos que substituir isso por uma função com valor de tabela em linha pode fornecer a reutilização de código sem a desagradável penalidade de desempenho. P:Podemos usar geradores de dados para obter o mesmo tamanho de dados para uso em desenvolvimento em vez de usar dados de produção (difíceis de obter)? A distorção de dados é importante para os planos resultantes?
R: A distorção de dados pode ter um fator, e suspeito que depende de que tipo de dados você está gerando/simulando e quão longe a distorção pode estar. Se você tiver, digamos, uma coluna varchar(100) que em produção normalmente tem 90 caracteres e sua geração de dados produz dados com média de 50 (que é o que o SQL Server assumirá), você encontrará um impacto muito diferente no número de páginas e otimização, e provavelmente testes não muito realistas.
Mas vou ser honesto:essa faceta específica não é algo em que investi muito tempo, porque geralmente consigo me forçar a conseguir dados reais. :-)
P:Todas as funções são criadas iguais ao examinar o desempenho da consulta? Se não, existe uma lista de funções conhecidas que você deve evitar quando possível?
R: Não, nem todas as funções são criadas iguais em termos de desempenho. Existem três tipos diferentes de funções que podemos criar (ignorando as funções CLR por enquanto):
- Funções escalares de várias instruções
- Funções com valor de tabela de várias instruções
- Funções com valor de tabela inline
Funções escalares inline são mencionadas na documentação, mas são um mito e, a partir do SQL Server 2014, pelo menos, também pode ser mencionado ao lado do Sasquatch e do Monstro do Lago Ness.
Em geral, e eu colocaria isso em fonte de 80pt se pudesse, funções com valor de tabela inline são boas, e as outras devem ser evitadas quando possível, pois são muito mais difíceis de otimizar.
As funções também podem ter propriedades diferentes que afetam seu desempenho, como se são determinísticas e se são vinculadas ao esquema.
Para muitos padrões de função, definitivamente há considerações de desempenho que você precisa fazer, e você também deve estar ciente deste item do Connect que visa abordá-las.
P:Podemos continuar executando os totais sem cursores?
R: Sim, nós podemos; existem vários métodos além de um cursor (conforme detalhado em minha postagem no blog, Melhores abordagens para totais em execução – atualizados para o SQL Server 2012):
- Subconsulta na lista SELECT
- CTE recursiva
- Auto-inscrição
- "Atualização peculiar"
- Somente SQL Server 2012+:SUM() OVER() (usando padrão / RANGE)
- Somente SQL Server 2012+:SUM() OVER() (usando ROWS)
A última opção é de longe a melhor abordagem se você estiver no SQL Server 2012; caso contrário, existem restrições nas outras opções que não são de cursor que muitas vezes tornarão um cursor a escolha mais atraente. Por exemplo, o método de atualização peculiar não está documentado e não é garantido que funcione na ordem que você espera; o CTE recursivo requer que não haja lacunas em qualquer mecanismo sequencial que você esteja usando; e as abordagens de subconsulta e autojunção simplesmente não são dimensionadas.