Muitas vezes vejo pessoas lutando com o SQL Server quando estão vendo dois planos de execução diferentes para o que acreditam ser a mesma consulta. Normalmente, isso é descoberto após outras observações, como tempos de execução muito diferentes. Eu digo que eles acreditam que é a mesma consulta porque, às vezes é, às vezes não.
Um dos casos mais comuns é quando eles estão testando uma consulta no SSMS e obtendo um plano diferente daquele obtido em seu aplicativo. Existem potencialmente dois fatores em jogo aqui (o que também pode ser relevante quando a comparação NÃO é entre o aplicativo e o SSMS):
- O aplicativo quase sempre tem
SET
diferente configurações do que SSMS (são coisas comoARITHABORT
,ANSI_NULLS
eQUOTED_IDENTIFIER
). Isso força o SQL Server a armazenar os dois planos separadamente; Erland Sommarskog tratou isso detalhadamente em seu artigo Slow in the Application, Fast in SSMS?
- Os parâmetros usados pelo aplicativo quando sua cópia do plano foi compilada pela primeira vez podem ter sido muito diferentes e levaram a um plano diferente daqueles usados na primeira vez que a consulta foi executada a partir do SSMS - isso é conhecido como sniffing de parâmetros . Erland fala sobre isso em profundidade também, e não vou regurgitar suas recomendações, mas resumir lembrando que testar a consulta do aplicativo no SSMS nem sempre é útil, pois é muito improvável que seja um teste de maçãs para maçãs.
Existem alguns outros cenários que são um pouco mais obscuros que trago em minha palestra sobre Maus Hábitos e Melhores Práticas. Esses são casos em que os planos não são diferentes, mas há várias cópias do mesmo plano sobrecarregando o cache do plano. Achei que deveria mencioná-los aqui porque eles sempre pegam muita gente de surpresa.
cAsE e espaços em branco são importantes
O SQL Server faz o hash do texto da consulta em um formato binário, o que significa que cada caractere no texto da consulta é crucial. Vamos fazer as seguintes consultas simples:
USE AdventureWorks2014; DBCC FREEPROCCACHE WITH NO_INFOMSGS; GO SELECT StoreID FROM Sales.Customer; GO -- original query GO SELECT StoreID FROM Sales.Customer; GO ----^---- extra space GO SELECT storeid FROM sales.customer; GO ---- lower case names GO select StoreID from Sales.Customer; GO ---- lower case keywords GO
Estes geram exatamente os mesmos resultados, obviamente, e geram exatamente o mesmo plano. No entanto, se observarmos o que temos no cache do plano:
SELECT t.[text], p.size_in_bytes, p.usecounts FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS t WHERE LOWER(t.[text]) LIKE N'%sales'+'.'+'customer%';
Os resultados são lamentáveis:
Portanto, neste caso, fica claro que maiúsculas e minúsculas são muito importantes. Falei sobre isso com muito mais detalhes em maio passado.
As referências de esquema são importantes
Eu já escrevi no blog antes sobre a importância de especificar o prefixo do esquema ao fazer referência a qualquer objeto, mas na época eu não estava totalmente ciente de que ele também tinha implicações de cache de plano.
Vamos dar uma olhada em um caso muito simples em que temos dois usuários com esquemas padrão diferentes e eles executam exatamente o mesmo texto de consulta, falhando ao referenciar o objeto por seu esquema:
USE AdventureWorks2014; DBCC FREEPROCCACHE WITH NO_INFOMSGS; GO CREATE USER SQLPerf1 WITHOUT LOGIN WITH DEFAULT_SCHEMA = Sales; CREATE USER SQLPerf2 WITHOUT LOGIN WITH DEFAULT_SCHEMA = Person; GO CREATE TABLE dbo.AnErrorLog(id INT); GRANT SELECT ON dbo.AnErrorLog TO SQLPerf1, SQLPerf2; GO EXECUTE AS USER = N'SQLPerf1'; GO SELECT id FROM AnErrorLog; GO REVERT; GO EXECUTE AS USER = N'SQLPerf2'; GO SELECT id FROM AnErrorLog; GO REVERT; GO
Agora, se dermos uma olhada no cache do plano, podemos extrair
sys.dm_exec_plan_attributes
para ver exatamente por que estamos recebendo dois planos diferentes para consultas idênticas:SELECT t.[text], p.size_in_bytes, p.usecounts, [schema_id] = pa.value, [schema] = s.name FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS t CROSS APPLY sys.dm_exec_plan_attributes(p.plan_handle) AS pa INNER JOIN sys.schemas AS s ON s.[schema_id] = pa.value WHERE t.[text] LIKE N'%AnError'+'Log%' AND pa.attribute = N'user_id';
Resultados:
E se você executar tudo novamente, mas adicione o
dbo.
prefixo para ambas as consultas, você verá que há apenas um plano que é usado duas vezes. Isso se torna um argumento muito convincente para sempre referenciar totalmente os objetos. SET configurações redux
Como uma observação lateral, você pode usar uma abordagem semelhante para determinar se
SET
as configurações são diferentes para duas ou mais versões da mesma consulta. Nesse caso, estamos investigando as consultas envolvidas com vários planos gerados por diferentes chamadas para o mesmo procedimento armazenado, mas você também pode identificá-las pelo texto da consulta ou pelo hash da consulta. SELECT p.plan_handle, p.usecounts, p.size_in_bytes, set_options = MAX(a.value) FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS t CROSS APPLY sys.dm_exec_plan_attributes(p.plan_handle) AS a WHERE t.objectid = OBJECT_ID(N'dbo.procedure_name') AND a.attribute = N'set_options' GROUP BY p.plan_handle, p.usecounts, p.size_in_bytes;
Se você tiver vários resultados aqui, deverá ver valores diferentes para
set_options
(que é uma máscara de bits). Isso é apenas o começo; Eu vou sair aqui e dizer que você pode determinar qual conjunto de opções está habilitado para cada plano descompactando o valor de acordo com a seção "Avaliando opções de conjunto" aqui. Sim, eu sou tão preguiçoso. Conclusão
Existem vários motivos pelos quais você pode ver planos diferentes para a mesma consulta (ou o que você acha que é a mesma consulta). Na maioria dos casos, você pode isolar a causa com bastante facilidade; o desafio é muitas vezes saber como procurá-lo em primeiro lugar. No meu próximo post, falarei sobre um assunto um pouco diferente:por que um banco de dados restaurado para um servidor "idêntico" pode render planos diferentes para a mesma consulta.