Alterar consulta para melhorar as estimativas do operador

14

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.

insira a descrição da imagem aqui

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:

  1. Por que a nomenclatura inicial ímpar no link pasteThePlan?

    Resposta : Como usei o plano de anonimização no SQL Sentry Plan Explorer.

  2. 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.

  3. 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 adianta SCHEMABINDINGaqui.

Respostas para mais perguntas possíveis:

  1. 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 .

  2. 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 INopção. Eu realmente não acho que haveria diferença, mesmo substituindo e, de qualquer forma, não há problema com isso.

Radu Gheorghiu
fonte
Eu acho que a #tempcriaçã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 isso in (select id from #temp)para uma existssubconsulta.
ypercubeᵀᴹ
@ ypercubeᵀᴹ Verdade, apenas algumas páginas a menos são lidas usando a variável em vez de uma tabela temporária.
Radu Gheorghiu
By the way, uma variável de tabela irá fornecer a estimativa contagem de linha correta quando usado com Option (Recompile) - mas ainda não tem estatísticas granulares, cardinalidade etc.
TH
Bem, eu olhei no plano de execução real nas estimativas, ao usar em select id from @customAttrValIdsvez de select id from #tempe o número estimado de linhas era 1para a variável e 3para #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.
Radu Gheorghiu
@RaduGheorghiu Sim, mas no mundo desses caras, a opção (recompilar) raramente é uma opção, e eles também preferem tabelas temporárias por outros motivos válidos. Talvez a estimativa simplesmente sempre mostre incorretamente como 1, pois altera o plano, conforme visto aqui: theboreddba.com/Categories/FunWithFlags/…
TH

Respostas:

12

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:

Correspondência de hash do nó 8 (agregado)

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 #temptabela 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 #TempTabledepois 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 #Temptabela 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 #Temp2deve 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):

# População Temp2

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:

Plano final

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.

Paul White 9
fonte
9

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:

desequilíbrio da linha

Como resultado, um thread realiza todo o trabalho com o índice:

busca de desequilíbrio de rosca

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 TOPoperador 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.

Joe Obbish
fonte
-2

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
3
Há um espaço muito grande de planos para essa consulta, com muitas opções para ordem de junção e aninhamento, paralelismo, agregação local / global etc. etc. a maioria seria afetada por alterações nas estatísticas derivadas (distribuição e cardinalidade bruta) no nó do plano 10. Observe também que as dicas de junção geralmente devem ser evitadas, pois elas vêm com um silêncio OPTION(FORCE ORDER), o que impede que o otimizador reordene as junções a partir da sequência textual e muitas outras otimizações.
Paul White 9
-12

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 SELECTtabela dinâmica de estilo para usar o PIVOToperador SQL padrão .

insert into Variable1 values (?), (?), (?)


select *
    into Object1
    from Variable2
        where Column1 is not null;



select Variable3 = Function1(distinct Column2) 
    from Object2 Object3
        inner join Object1 Object4 on Object3.Column1 = Object4.Column1;



select Object4.Column1
        , Object4.Column3 
    from Object5 Object4
        inner join Object6 Object7
            on Object4.Column1 = Object7.Column4
        inner join Object2 Object8 
            on Object8.Column1 = Object7.Column5
    where Object4.Column6 = Variable4
        and Object7.Column5 in (select Column1 from Object1)
    group by Object4.Column3
        , Object4.Column1
    having Function1(distinct Object8.Column2) = Variable3
    option(recompile);

Então, vamos ver, SELECT * INTOgeralmente é menos eficiente que um padrão INSERT Object1 (column list) SELECT column list. Então eu reescreveria isso. Em seguida, se Função1 foi definida sem a WITH SCHEMABINDING, a adição de uma WITH SCHEMABINDINGclá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)".

INcláusulas dessa natureza são sempre mais eficientes escritas como EXISTS (SELECT 1 FROM Object1 o1 WHERE o1.Column1 = Object7.Column5). Talvez eu devesse ter escrito isso de outra maneira. EXISTSsempre será pelo menos tão bom quanto IN. Nem sempre é melhor, mas geralmente é.

Além disso, duvido que option(recompile)esteja melhorando o desempenho da consulta aqui. Eu testaria removê-lo.

Matthew Sontum
fonte
6
Se uma pesquisa de índice não clusterizado cobrir a consulta, quase sempre será melhor que uma pesquisa de índice clusterizado, porque, por definição, o índice clusterizado possui todas as colunas e o índice não clusterizado possui menos colunas, portanto, requer menos pesquisas de página (e menos níveis de etapas na árvore b) para recuperar os dados. Portanto, não é preciso dizer que uma busca de índice em cluster sempre será melhor.
ErikE 26/02