Eu tenho uma consulta que é executada em um período de tempo aceitável, mas quero diminuir o máximo de desempenho possível.
A operação que estou tentando melhorar é a "Busca de índice" à direita do plano, do Nó 17.
Adicionei índices apropriados, mas as estimativas que recebo para essa operação são metade do que deveriam ser.
Procurei alterar meus índices, adicionar uma tabela temporária e reescrever a consulta, mas não pude simplificá-la mais do que isso para obter as estimativas corretas.
Alguém tem alguma sugestão sobre o que mais eu posso tentar?
O plano completo e seus detalhes podem ser encontrados aqui .
O plano não-anonimizado pode ser encontrado aqui.
Atualizar:
Sinto que a versão inicial da pergunta levantou muita confusão, então adicionarei o código original com algumas explicações.
create procedure [dbo].[someProcedure] @asType int, @customAttrValIds idlist readonly
as
begin
set nocount on;
declare @dist_ca_id int;
select *
into #temp
from @customAttrValIds
where id is not null;
select @dist_ca_id = count(distinct CustomAttrID)
from CustomAttributeValues c
inner join #temp a on c.Id = a.id;
select a.Id
, a.AssortmentId
from Assortments a
inner join AssortmentCustomAttributeValues acav
on a.Id = acav.Assortment_Id
inner join CustomAttributeValues cav
on cav.Id = acav.CustomAttributeValue_Id
where a.AssortmentType = @asType
and acav.CustomAttributeValue_Id in (select id from #temp)
group by a.AssortmentId
, a.Id
having count(distinct cav.CustomAttrID) = @dist_ca_id
option(recompile);
end
Respostas:
Por que a nomenclatura inicial ímpar no link pasteThePlan?
Resposta : Como usei o plano de anonimização no SQL Sentry Plan Explorer.
Por que
OPTION RECOMPILE
?Resposta : Como posso recompilar para evitar a detecção de parâmetros (os dados são / podem estar distorcidos). Eu testei e estou feliz com o plano que o Optimizer gera durante o uso
OPTION RECOMPILE
.WITH SCHEMABINDING
?Resposta : Eu realmente gostaria de evitar isso e o usaria apenas quando tiver uma exibição indexada. De qualquer forma, esta é uma função do sistema (
COUNT()
), então não adiantaSCHEMABINDING
aqui.
Respostas para mais perguntas possíveis:
Por que eu uso
INSERT INTO #temp FROM @customAttrributeValues
?Resposta : Como eu notei e agora sei que, ao usar variáveis conectadas a uma consulta, todas as estimativas que saem do trabalho com uma variável são sempre 1. E eu testei colocando os dados em uma tabela temporária e o Estimado é igual a Linhas reais .
Por que eu usei
and acav.CustomAttributeValue_Id in (select id from #temp)
?Resposta : Eu poderia ter substituído por um JOIN no #temp, mas os desenvolvedores ficaram muito confusos e ofereceram a
IN
opção. Eu realmente não acho que haveria diferença, mesmo substituindo e, de qualquer forma, não há problema com isso.
fonte
#temp
criação e o uso seriam um problema de desempenho, não um ganho. Você está salvando em uma tabela não indexada apenas para ser usada uma vez. Tente removê-lo completamente (e possivelmente mudar issoin (select id from #temp)
para umaexists
subconsulta.select id from @customAttrValIds
vez deselect id from #temp
e o número estimado de linhas era1
para a variável e3
para #temp (que correspondia ao número real de linhas). É por isso que eu substituí@
por#
. E eu FAZER lembrar de uma conversa (de Brent O ou Aaron Bertrand), onde eles disseram que quando se utiliza uma variável tbl as estimativas de que será sempre 1. E como uma melhoria para obter melhores estimativas eles usariam uma tabela temporária.Respostas:
O plano foi compilado em uma instância do SQL Server 2008 R2 RTM (compilação 10.50.1600). Você deve instalar o Service Pack 3 (compilação 10.50.6000), seguido pelos patches mais recentes para trazê-lo para a compilação mais recente (atual) 10.50.6542. Isso é importante por vários motivos, incluindo segurança, correções de bugs e novos recursos.
Otimização de incorporação de parâmetros
Relevante à presente pergunta, o SQL Server 2008 R2 RTM não suportava o PEO (Parameter Embedding Optimization) para
OPTION (RECOMPILE)
. No momento, você está pagando o custo das recompilações sem perceber um dos principais benefícios.Quando o PEO está disponível, o SQL Server pode usar os valores literais armazenados em variáveis e parâmetros locais diretamente no plano de consulta. Isso pode levar a simplificações dramáticas e aumentos de desempenho. Há mais informações sobre isso no meu artigo, Detecção de parâmetros, incorporação e as opções RECOMPILE .
Derramamentos de hash, classificação e troca
Eles são exibidos apenas nos planos de execução quando a consulta foi compilada no SQL Server 2012 ou posterior. Nas versões anteriores, tivemos que monitorar derramamentos enquanto a consulta estava sendo executada usando o Profiler ou Eventos Estendidos. Os derramamentos sempre resultam em E / S física para (e de) o tempdb de backup de armazenamento persistente , que pode ter importantes conseqüências de desempenho, especialmente se o derramamento for grande ou se o caminho de E / S estiver sob pressão.
No seu plano de execução, existem dois operadores de Hash Match (Agregado). A memória reservada para a tabela de hash é baseada na estimativa para as linhas de saída (em outras palavras, é proporcional ao número de grupos encontrados no tempo de execução). A memória concedida é corrigida imediatamente antes do início da execução e não pode crescer durante a execução, independentemente da quantidade de memória livre que a instância possui. No plano fornecido, os dois operadores Hash Match (Agregado) produzem mais linhas do que o otimizador esperado e, portanto, podem estar passando por um derramamento no tempdb durante a execução.
Há também um operador Hash Match (Inner Join) no plano. A memória reservada para a tabela de hash é baseada na estimativa para as linhas de entrada do lado da sonda . A entrada do probe estima 847.399 linhas, mas 1.223.636 são encontradas no tempo de execução. Esse excesso também pode estar causando um derramamento de hash.
Agregado redundante
A Hash Match (Agregado) no nó 8 executa uma operação de agrupamento ativada
(Assortment_Id, CustomAttrID)
, mas as linhas de entrada são iguais às linhas de saída:Isso sugere que a combinação de colunas é uma chave (portanto, o agrupamento é semanticamente desnecessário). O custo da execução do agregado redundante é aumentado pela necessidade de passar 1,4 milhão de linhas duas vezes entre trocas de particionamento de hash (os operadores de paralelismo de ambos os lados).
Como as colunas envolvidas vêm de tabelas diferentes, é mais difícil do que o habitual comunicar essas informações de exclusividade ao otimizador, para evitar a operação de agrupamento redundante e trocas desnecessárias.
Distribuição ineficiente de threads
Conforme observado na resposta de Joe Obbish , a troca no nó 14 usa particionamento de hash para distribuir linhas entre os threads. Infelizmente, o pequeno número de linhas e os agendadores disponíveis significam que as três linhas terminam em um único encadeamento. O plano aparentemente paralelo é executado em série (com sobrecarga paralela) até a troca no nó 9.
Você pode resolver isso (para obter particionamento round-robin ou broadcast) eliminando a Classificação distinta no nó 13. A maneira mais fácil de fazer isso é criar uma chave primária em cluster na
#temp
tabela e executar a operação distinta ao carregar a tabela:Armazenamento em cache de estatísticas da tabela temporária
Apesar do uso
OPTION (RECOMPILE)
, o SQL Server ainda pode armazenar em cache o objeto de tabela temporário e suas estatísticas associadas entre chamadas de procedimento. Isso geralmente é uma otimização de desempenho bem-vinda, mas se a tabela temporária for preenchida com uma quantidade semelhante de dados em chamadas de procedimento adjacentes, o plano recompilado poderá ser baseado em estatísticas incorretas (armazenadas em cache de uma execução anterior). Isso está detalhado em meus artigos, Tabelas temporárias em procedimentos armazenados e cache de tabelas temporárias explicado .Para evitar isso, use
OPTION (RECOMPILE)
junto com um explícitoUPDATE STATISTICS #TempTable
depois que a tabela temporária for preenchida e antes de ser referenciada em uma consulta.Reescrever consulta
Esta parte pressupõe que as alterações na criação da
#Temp
tabela já foram feitas.Dados os custos de possíveis derramamentos de hash e o agregado redundante (e trocas adjacentes), pode pagar para materializar o conjunto no nó 10:
Ele
PRIMARY KEY
é adicionado em uma etapa separada para garantir que a compilação do índice tenha informações precisas de cardinalidade e para evitar o problema de cache temporário das estatísticas da tabela.É provável que essa materialização ocorra na memória (evitando tempdb I / O) se a instância tiver memória suficiente disponível. Isso é ainda mais provável quando você atualiza para o SQL Server 2012 (SP1 CU10 / SP2 CU1 ou posterior), que melhorou o comportamento do Eager Write .
Essa ação fornece ao otimizador informações precisas sobre cardinalidade no conjunto intermediário, permite a criação de estatísticas e a declaração
(Assortment_Id, CustomAttrID)
como chave.O plano para a população de
#Temp2
deve ficar assim (observe a varredura de índice em cluster de#Temp
, sem classificação distinta e a troca agora usa o particionamento de linha de rodízio):Com esse conjunto disponível, a consulta final se torna:
Poderíamos reescrever manualmente
COUNT_BIG(DISTINCT...
como simplesCOUNT_BIG(*)
, mas com as novas informações importantes, o otimizador faz isso por nós:O plano final pode usar uma junção loop / hash / mesclagem, dependendo das informações estatísticas sobre os dados aos quais eu não tenho acesso. Mais uma pequena nota: presumi que
CREATE [UNIQUE?] NONCLUSTERED INDEX IX_ ON dbo.Assortments (AssortmentType, Id, AssortmentId);
exista um índice semelhante .De qualquer forma, o importante sobre os planos finais é que as estimativas devem ser muito melhores, e a sequência complexa de operações de agrupamento foi reduzida a um único Stream Aggregate (que não requer memória e, portanto, não pode se espalhar para o disco).
É difícil dizer que o desempenho será realmente melhor nesse caso com a tabela temporária extra, mas as estimativas e as escolhas do plano serão muito mais resistentes a alterações no volume e distribuição de dados ao longo do tempo. Isso pode ser mais valioso a longo prazo do que um pequeno aumento no desempenho hoje. De qualquer forma, agora você tem muito mais informações para basear sua decisão final.
fonte
As estimativas de cardinalidade na sua consulta são realmente muito boas. É raro obter o número estimado de linhas para corresponder exatamente ao número real de linhas, especialmente quando você tem tantas associações. As estimativas de cardinalidade de junção são complicadas para o otimizador acertar. Uma coisa importante a ser observada é que o número de linhas estimadas para a parte interna do loop aninhado é por execução desse loop. Portanto, quando o SQL Server diz que 463869 linhas serão buscadas com o índice, a estimativa real nesse caso é o número de execuções (2) * 463869 = 927738, que não está muito longe do número real de linhas, 1391608. Surpreendentemente, o número de linhas estimadas está quase perfeito imediatamente após a junção do loop aninhado no ID do nó 10.
As estimativas de cardinalidade insuficiente são principalmente um problema quando o otimizador de consultas escolhe o plano errado ou não concede memória suficiente ao plano. Não vejo derramamentos no tempdb para esse plano, portanto a memória parece boa. Para a junção de loop aninhada que você chama, há uma pequena tabela externa e uma tabela interna indexada. O que há de errado nisso? Para ser mais preciso, o que você esperaria que o otimizador de consultas fizesse diferente aqui?
Em termos de melhoria de desempenho, o que mais me destaca é que o SQL Server está usando um algoritmo de hash para distribuir linhas paralelas, o que resulta em todos eles no mesmo segmento:
Como resultado, um thread realiza todo o trabalho com o índice:
Isso significa que sua consulta efetivamente não é executada em paralelo até que a repartição transmita o operador no ID do nó 9. O que você provavelmente deseja é o particionamento round robin para que cada linha termine em seu próprio encadeamento. Isso permitirá que dois encadeamentos procurem o ID do nó 17. O acréscimo de um
TOP
operador supérfluo pode facilitar o particionamento de rodízio. Posso adicionar detalhes aqui, se quiser.Se você realmente deseja se concentrar nas estimativas de cardinalidade, poderá colocar as linhas após a primeira junção em uma tabela temporária. Se você reunir estatísticas na tabela temporária que fornece ao otimizador mais informações sobre a tabela externa para a junção de loop aninhado que você chamou. Também pode resultar no particionamento de rodízio.
Se você não estiver usando sinalizadores de rastreamento 4199 ou 2301, considere-os. O sinalizador de rastreamento 4199 oferece uma ampla variedade de correções do otimizador, mas elas podem degradar algumas cargas de trabalho. O sinalizador de rastreamento 2301 altera algumas das suposições de cardinalidade de junção do otimizador de consulta e torna o trabalho mais difícil. Nos dois casos, teste cuidadosamente antes de habilitá-los.
fonte
Acredito que obter uma estimativa melhor dessa junção não mudará o plano, a menos que 1,4 milhão seja uma parte suficiente da tabela para fazer o otimizador escolher uma varredura de índice (não cluster) com junção de hash ou mesclagem. Suspeito que não seria o caso aqui, nem realmente útil, mas você pode testar os efeitos substituindo a junção interna contra CustomAttributeValues por junção de hash interna e junção de mesclagem interna .
Também examinei o código de maneira mais ampla e não vejo como melhorá-lo - eu estaria interessado em provar que está errado, é claro. E se você quiser publicar a lógica completa do que está tentando realizar, eu estaria interessado em outro olhar.
fonte
OPTION(FORCE ORDER)
, o que impede que o otimizador reordene as junções a partir da sequência textual e muitas outras otimizações.Você não vai melhorar de uma busca de índice [não agrupada]. A única coisa melhor do que uma busca de índice não em cluster é uma busca de índice em cluster.
Além disso, eu tenho um DBA do SQL nos últimos dez anos e um desenvolvedor do SQL há cinco anos, e, na minha experiência, é extremamente raro encontrar uma melhoria em uma Consulta do SQL estudando o plano de execução que você não conseguiu ' Não encontre por outros meios. O principal motivo para gerar o plano de execução é que ele geralmente sugere índices ausentes que você pode adicionar para melhorar o desempenho.
Os principais ganhos de desempenho serão no ajuste da própria Consulta SQL, se houver alguma ineficiência lá. Por exemplo, há alguns meses, consegui uma função SQL para executar 160 vezes mais rápido, reescrevendo uma
SELECT UNION SELECT
tabela dinâmica de estilo para usar oPIVOT
operador SQL padrão .Então, vamos ver,
SELECT * INTO
geralmente é menos eficiente que um padrãoINSERT Object1 (column list) SELECT column list
. Então eu reescreveria isso. Em seguida, se Função1 foi definida sem aWITH SCHEMABINDING
, a adição de umaWITH SCHEMABINDING
cláusula deve permitir que ela seja executada mais rapidamente.Você escolheu vários aliases que não fazem sentido, como aliar Objeto2 como Objeto3. Você deve escolher aliases melhores que não ofuscam o código. Você tem "Object7.Column5 em (selecione Coluna1 do Objeto1)".
IN
cláusulas dessa natureza são sempre mais eficientes escritas comoEXISTS (SELECT 1 FROM Object1 o1 WHERE o1.Column1 = Object7.Column5)
. Talvez eu devesse ter escrito isso de outra maneira.EXISTS
sempre será pelo menos tão bom quantoIN
. Nem sempre é melhor, mas geralmente é.Além disso, duvido que
option(recompile)
esteja melhorando o desempenho da consulta aqui. Eu testaria removê-lo.fonte