Cómo optimizar una consulta que se ejecuta lentamente en Nested Loops (Inner Join)

39

TL; DR

Como esta pregunta sigue obteniendo puntos de vista, la resumiré aquí para que los recién llegados no tengan que sufrir la historia:

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

Me doy cuenta de que esto podría no ser un problema de todos, pero al resaltar la sensibilidad de las cláusulas ON, podría ayudarlo a mirar en la dirección correcta. En cualquier caso, el texto original está aquí para futuros antropólogos:

Texto original

Considere la siguiente consulta simple (solo 3 tablas involucradas)

    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

Esta es una consulta bastante simple, la única parte confusa es la última unión de categoría, es así porque el nivel de categoría 5 puede o no existir. Al final de la consulta, estoy buscando información de categoría por ID de producto (ID de SKU), y ahí es donde entra la tabla muy grande category_link. Finalmente, la tabla #Ids es solo una tabla temporal que contiene 10'000 Ids.

Cuando se ejecuta, obtengo el siguiente plan de ejecución real:

Plan de ejecución real

Como puede ver, casi el 90% del tiempo se gasta en los bucles anidados (unión interna). Aquí hay información adicional sobre esos bucles anidados:

Bucles anidados (unión interna)

Tenga en cuenta que los nombres de la tabla no coinciden exactamente porque edité los nombres de la tabla de consulta para facilitar su lectura, pero es bastante fácil de combinar (ads_alt_category = category). ¿Hay alguna forma de optimizar esta consulta? También tenga en cuenta que en producción, la tabla temporal #Ids no existe, es un parámetro de valor de tabla de los mismos 10'000 Ids pasados ​​al procedimiento almacenado.

Información adicional:

  • índices de categoría en category_id y parent_category_id
  • category_voc index en category_id, language_code
  • category_link index en sku_id, category_id

Editar (resuelto)

Como se señaló en la respuesta aceptada, el problema era la cláusula OR en la categoría JOIN. Sin embargo, el código sugerido en la respuesta aceptada es muy lento, incluso más lento que el código original. Una solución mucho más rápida y también más limpia es simplemente reemplazar la condición JOIN actual con lo siguiente:

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

Este ajuste minucioso es la solución más rápida, probado contra la doble unión de la respuesta aceptada y también probado contra la APLICACIÓN CRUZADA según lo sugerido por valverij.

Luis Ferrao
fuente
Necesitaremos ver el resto del plan de consulta.
RBarryYoung
Solo una observación: con tantas uniones dependientes, los errores de estimación de cardinalidad se vuelven probables. Muy a menudo, el rendimiento de la consulta se desvía por la subestimación de la cardinalidad.
usr
¿El plan de ejecución hace sugerencias para los índices? Además, no olvide que puede establecer claves e índices primarios en sus tablas temporales (más información aquí )
@rbarry Si después de probar las soluciones actuales no obtengo nada, mejoraré la pregunta
1
¿Qué hay de duplicar la consulta con UNION y deshacerse del OR

Respuestas:

17

El problema parece estar en esta parte del 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
)

oren condiciones de unión siempre es sospechoso. Una sugerencia es dividir esto en dos combinaciones:

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

Luego tiene que modificar el resto de la consulta para manejar esto. . . coalesce(l1.sku_id, l2.sku_id)por ejemplo en la selectcláusula

Gordon Linoff
fuente
Con la cantidad de filtrado que se realiza en ese particular, se unen, también había prueba cambiando la JOINde una CROSS APPLYcon la INconmutación a una EXISTSen el APPLY's WHEREcláusula.
Gracias Gordon, probaré esto a primera hora de la mañana. @ Valverij, no estoy familiarizado con la aplicación cruzada, ¿podría describir su solución más, tal vez en una respuesta adecuada, para que pueda votar si resulta ser el escenario más rápido?
3
Estoy aceptando esta respuesta porque fue la primera respuesta que me señaló el problema. Sin embargo, la solución sugerida es extremadamente lenta, incluso más lenta que el código original. Sin embargo, sabiendo que la cláusula OR era el problema, simplemente reemplazarla ON l.category_id = ISNULL(c5.category_id, c4.category_idhizo el truco.
Luis Ferrao
1
@LuisFerrao. . . Gracias por la información adicional. Es útil saber que coalesce()empuja al optimizador en la dirección correcta.
Gordon Linoff
9

Como otro usuario mencionó, esta unión es probablemente la 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
)

Además de dividirlos en varias combinaciones, también puede probar un 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

Desde el enlace de MSDN arriba:

La función con valores de tabla actúa como la entrada derecha y la expresión de tabla externa actúa como la entrada izquierda. La entrada derecha se evalúa para cada fila desde la entrada izquierda y las filas producidas se combinan para la salida final .

Básicamente, APPLYes como una subconsulta que filtra los registros de la derecha primero y luego aplica al resto de su consulta.

Este artículo hace un muy buen trabajo al explicar qué es y cuándo usarlo: http://explainextended.com/2009/07/16/inner-join-vs-cross-apply/

Sin embargo, es importante tener en cuenta que CROSS APPLYno siempre funciona más rápido que unINNER JOIN . En muchas situaciones, probablemente será lo mismo. Sin embargo, en casos raros, lo he visto más lento (de nuevo, todo esto depende de la estructura de su tabla y de la consulta en sí).

Como regla general, si me encuentro uniéndome a una tabla con demasiadas declaraciones condicionales, entonces tiendo a inclinarme hacia APPLY

También una nota divertida: OUTER APPLYactuará como unLEFT JOIN

Además, tome nota de mi elección para usar en EXISTSlugar de IN. Al realizar INuna subconsulta, recuerde que devolverá todo el conjunto de resultados, incluso después de haber encontrado su valor. Con EXISTS, sin embargo, detendrá la subconsulta en el instante en que encuentre una coincidencia.

valverij
fuente
Probé esta solución a fondo. Según lo escribió, es bastante lento, pero olvidó aplicar el consejo con el que comenzó su mensaje. Sustitución AND x.cat = c4.cat OR x.cat = c5.catpor x.cat = ISNULL(c5.cat, c4.cat)y deshacerse de la cláusula IN hizo esta la solución de segundo más rápido, y digno de un upvote, porque es bastante informativo.
Luis Ferrao
Gracias. En realidad, no se suponía que la línea IN estuviera allí (no podía decidir usar IN o seguir con el OR), la eliminaré.
valverij