Como otimizar uma consulta que está executando lentamente em loops aninhados (associação interna)

39

TL; DR

Como essa pergunta continua sendo exibida, vou resumir aqui para que os novatos não precisem sofrer a história:

JOIN table t ON t.member = @value1 OR t.member = @value2 -- this is slow as hell
JOIN table t ON t.member = COALESCE(@value1, @value2)    -- this is blazing fast
-- Note that here if @value1 has a value, @value2 is NULL, and vice versa

Sei que isso pode não ser problema de todos, mas, ao destacar a sensibilidade das cláusulas ON, isso pode ajudá-lo a olhar na direção certa. De qualquer forma, o texto original está aqui para futuros antropólogos:

Texto original

Considere a seguinte consulta simples (apenas 3 tabelas envolvidas)

    SELECT

        l.sku_id AS ProductId,
        l.is_primary AS IsPrimary,
        v1.category_name AS Category1,
        v2.category_name AS Category2,
        v3.category_name AS Category3,
        v4.category_name AS Category4,
        v5.category_name AS Category5

    FROM category c4
    JOIN category_voc v4 ON v4.category_id = c4.category_id and v4.language_code = 'en'

    JOIN category c3 ON c3.category_id = c4.parent_category_id
    JOIN category_voc v3 ON v3.category_id = c3.category_id and v3.language_code = 'en'

    JOIN category c2 ON c2.category_id = c3.category_id
    JOIN category_voc v2 ON v2.category_id = c2.category_id and v2.language_code = 'en'

    JOIN category c1 ON c1.category_id = c2.parent_category_id
    JOIN category_voc v1 ON v1.category_id = c1.category_id and v1.language_code = 'en'

    LEFT OUTER JOIN category c5 ON c5.parent_category_id = c4.category_id
    LEFT OUTER JOIN category_voc v5 ON v5.category_id = c5.category_id and v5.language_code = @lang

    JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
    (
        l.category_id = c4.category_id OR
        l.category_id = c5.category_id
    )

    WHERE c4.[level] = 4 AND c4.version_id = 5

Essa é uma consulta bastante simples, a única parte confusa é a última ingresso na categoria, é assim porque o nível de categoria 5 pode ou não existir. No final da consulta, estou procurando informações de categoria por ID do produto (SKU ID) e é aí que entra a tabela muito grande category_link. Finalmente, a tabela #Ids é apenas uma tabela temporária que contém 10.000 IDs.

Quando executado, recebo o seguinte plano de execução real:

Plano de execução real

Como você pode ver, quase 90% do tempo é gasto nos loops aninhados (união interna). Aqui estão informações adicionais sobre esses loops aninhados:

Loops aninhados (junção interna)

Observe que os nomes das tabelas não correspondem exatamente porque eu editei os nomes das tabelas de consulta para facilitar a leitura, mas é muito fácil corresponder (ads_alt_category = category). Existe alguma maneira de otimizar esta consulta? Observe também que, na produção, a tabela temporária #Ids não existe, é um parâmetro com valor de tabela com os mesmos 10.000 IDs passados ​​para o procedimento armazenado.

Informação adicional:

  • índices de categoria em category_id e parent_category_id
  • índice category_voc em category_id, language_code
  • category_link index em sku_id, category_id

Editar (resolvido)

Conforme apontado pela resposta aceita, o problema era a cláusula OR no category_link JOIN. No entanto, o código sugerido na resposta aceita é muito lento, mais lento que o código original. Uma solução muito mais rápida e também muito mais limpa é simplesmente substituir a condição atual de JOIN pela seguinte:

JOIN category_link l on l.sku_id IN (SELECT value FROM @p1) AND l.category_id = COALESCE(c5.category_id, c4.category_id)

Esse ajuste de minuto é a solução mais rápida, testada contra a junção dupla da resposta aceita e também testada contra o CROSS APPLY, conforme sugerido por valverij.

Luis Ferrao
fonte
Precisamos ver o restante do plano de consulta.
usar o seguinte código
Apenas uma observação: com esse número de junções dependentes, erros de estimativa de cardinalidade se tornam prováveis. Na maioria das vezes, o desempenho da consulta é prejudicado pela subestimação da cardinalidade.
usr
O plano de execução faz sugestões para índices? Além disso, não se esqueça que você pode definir as chaves primárias e índices em suas tabelas temporárias (mais informações aqui )
@rbarry Se depois de tentar as soluções atuais Eu não tenho nada, eu vou melhorar na questão
11
E quanto a duplicar a consulta com um UNION e se livrar do OR

Respostas:

17

O problema parece estar nesta parte do código:

JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)

orem condições de junção é sempre suspeito. Uma sugestão é dividir isso em duas junções:

JOIN category_link l1 on l1.sku_id in (SELECT value FROM #Ids) and l1.category_id = cr.category_id
left outer join
category_link l1 on l2.sku_id in (SELECT value FROM #Ids) and l2.category_id = cr.category_id

Você precisará modificar o restante da consulta para lidar com isso. . . coalesce(l1.sku_id, l2.sku_id)por exemplo na selectcláusula.

Gordon Linoff
fonte
Com a quantidade de filtragem sendo feita nessa junção específica, eu também testaria a alteração de JOINpara um CROSS APPLYcom a INmudança para um EXISTSna cláusula APPLY's WHERE.
Obrigado Gordon, vou testar essa primeira coisa de manhã. @Valverij, eu não estou familiarizado com a aplicação cruzada, você poderia descrever sua solução mais, talvez em uma resposta adequada, para que eu possa votar se for o cenário mais rápido?
3
Estou aceitando esta resposta porque foi a primeira resposta que me indicou o problema. A solução sugerida, no entanto, é extremamente lenta, mais lenta que o código original. No entanto, sabendo que a cláusula OR era o problema, basta substituí-la por ON l.category_id = ISNULL(c5.category_id, c4.category_idisso.
Luis Ferrão
11
@LuisFerrao. . . Obrigado pela informação adicional. É útil saber que ele coalesce()empurra o otimizador na direção certa.
Gordon Linoff
9

Como outro usuário mencionado, essa associação provavelmente é a causa:

JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)

Além de dividi-las em várias junções, você também pode tentar CROSS APPLY

CROSS APPLY (
    SELECT [some column(s)]
    FROM category_link x
    WHERE EXISTS(SELECT value FROM #Ids WHERE value = x.sku_id)
    AND (x.category_id = c4.category_id OR x.category_id = c5.category_id)        
) l

No link MSDN acima:

A função com valor de tabela atua como a entrada direita e a expressão da tabela externa atua como a entrada esquerda. A entrada direita é avaliada para cada linha da entrada esquerda e as linhas produzidas são combinadas para a saída final .

Basicamente, APPLYé como uma subconsulta que filtra os registros à direita primeiro e depois os aplica ao restante da sua consulta.

Este artigo explica muito bem o que é e quando usá-lo: http://explainextended.com/2009/07/16/inner-join-vs-cross-apply/

É importante notar, no entanto, que CROSS APPLYnem sempre o desempenho é mais rápido que um INNER JOIN. Em muitas situações, provavelmente será o mesmo. Em casos raros, porém, eu já o vi mais devagar (mais uma vez, tudo depende da estrutura da tabela e da própria consulta).

Como regra geral, se eu me vejo entrando em uma tabela com muitas declarações condicionais, então tendem a me inclinar para APPLY

Também uma observação divertida: OUTER APPLYagirá como umLEFT JOIN

Além disso, observe a minha escolha de usar em EXISTSvez de IN. Ao fazer INuma subconsulta, lembre-se de que ele retornará todo o conjunto de resultados, mesmo depois de encontrar seu valor. Com EXISTS, porém, ele interromperá a subconsulta no instante em que encontrar uma correspondência.

valverij
fonte
Eu testei esta solução completamente. Enquanto você escrevia, é bem lento, mas você esqueceu de aplicar os conselhos com os quais iniciou sua mensagem. Substituir AND x.cat = c4.cat OR x.cat = c5.catpor x.cat = ISNULL(c5.cat, c4.cat)e livrar-se da cláusula IN fez esta solução o segundo mais rápido, e digno de um upvote, porque é muito informativo.
Luis Ferrão
Obrigado. Na verdade, a linha IN não deveria estar lá (não foi possível decidir usar IN ou manter a OR), eu a removerei.
Valverij 18/04