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:
CREATE TABLE #Temp
(
id integer NOT NULL PRIMARY KEY CLUSTERED
);
INSERT #Temp
(
id
)
SELECT DISTINCT
CAV.id
FROM @customAttrValIds AS CAV
WHERE
CAV.id IS NOT NULL;
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ícito UPDATE 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:
CREATE TABLE #Temp2
(
CustomAttrID integer NOT NULL,
Assortment_Id integer NOT NULL,
);
INSERT #Temp2
(
Assortment_Id,
CustomAttrID
)
SELECT
ACAV.Assortment_Id,
CAV.CustomAttrID
FROM #temp AS T
JOIN dbo.CustomAttributeValues AS CAV
ON CAV.Id = T.id
JOIN dbo.AssortmentCustomAttributeValues AS ACAV
ON T.id = ACAV.CustomAttributeValue_Id;
ALTER TABLE #Temp2
ADD CONSTRAINT PK_#Temp2_Assortment_Id_CustomAttrID
PRIMARY KEY CLUSTERED (Assortment_Id, CustomAttrID);
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:
SELECT
A.Id,
A.AssortmentId
FROM
(
SELECT
T.Assortment_Id
FROM #Temp2 AS T
GROUP BY
T.Assortment_Id
HAVING
COUNT_BIG(DISTINCT T.CustomAttrID) = @dist_ca_id
) AS DT
JOIN dbo.Assortments AS A
ON A.Id = DT.Assortment_Id
WHERE
A.AssortmentType = @asType
OPTION (RECOMPILE);
Poderíamos reescrever manualmente COUNT_BIG(DISTINCT...
como simples COUNT_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.
#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.