¿Por qué agregar un TOP 1 empeora dramáticamente el rendimiento?

39

Tengo una consulta bastante simple

SELECT TOP 1 dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

Eso me está dando un rendimiento horrible (como nunca molestarse en esperar a que termine). El plan de consulta se ve así:

ingrese la descripción de la imagen aquí

Sin embargo, si elimino TOP 1, obtengo un plan que se ve así y se ejecuta en 1-2 segundos:

ingrese la descripción de la imagen aquí

Corregir PK e indexación a continuación.

El hecho de que haya TOP 1cambiado el plan de consulta no me sorprende, solo estoy un poco sorprendido de que lo haga mucho peor.

Nota: He leído los resultados de esta publicación y entiendo el concepto de un Row Goaletc. Lo que me interesa es cómo puedo cambiar la consulta para que utilice el mejor plan. Actualmente estoy volcando los datos en una tabla temporal y luego quitando la primera fila. Me pregunto si hay un mejor método.

Editar Para las personas que leen esto después del hecho, aquí hay algunas piezas adicionales de información.

  • Document_Queue: PK / CI es D_ID y tiene ~ 5k filas.
  • Correspondence_Journal: PK / CI es FILE_NUMBER, CORRESPONDENCE_ID y tiene ~ 1.4 mil filas.

Cuando comencé no había otros índices. Terminé con uno en Correspondence_Journal (Document_Id, File_Number)

Kenneth Fisher
fuente
1
¿Tiene una restricción de clave externa que imponga la DOCUMENT_IDrelación entre las dos tablas (o cada registro CORRESPONDENCE_JOURNALtiene un registro coincidente DOCUMENT_QUEUE)?
Daniel Hutmacher

Respuestas:

28

Intenta forzar un hash join *

SELECT TOP 1 
       dc.DOCUMENT_ID,
       dc.COPIES,
       dc.REQUESTOR,
       dc.D_ID,
       cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
INNER HASH JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
       AND dc.QUEUE_DATE <= GETDATE()
       AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

El optimizador probablemente pensó que un bucle iba a ser mejor con el top 1 y eso tiene sentido, pero en realidad no funcionó aquí. Solo una suposición aquí, pero tal vez el costo estimado de ese carrete estaba apagado; usa TEMPDB; es posible que tenga un TEMPDB de bajo rendimiento.


* Tenga cuidado con las sugerencias de combinación , ya que fuerzan el orden de acceso a la tabla del plan para que coincida con el orden escrito de las tablas en la consulta (como si se OPTION (FORCE ORDER)hubiera especificado). Desde el enlace de documentación:

Extracto de BOL

Es posible que esto no produzca ningún efecto indeseable en el ejemplo, pero en general, podría muy bien. FORCE ORDER(implícito o explícito) es una pista muy poderosa que va más allá de imponer el orden; evita que se aplique una amplia gama de técnicas optimizadoras, incluidas agregaciones parciales y reordenamientos.

Una sugerencia de OPTION (HASH JOIN) consulta puede ser menos intrusiva en casos adecuados, ya que esto no implica FORCE ORDER. Sin embargo, se aplica a todas las combinaciones en la consulta. Otras soluciones están disponibles.

paparazzo
fuente
1
Parece que la respuesta correcta y la única diferencia entre este y el plan más simple fue una clasificación adicional al frente.
Kenneth Fisher
3
No estoy seguro de que me guste esta respuesta. Unir pistas son muy invasivas. Primero se deben intentar algunos cambios de indexación simples, por ejemplo, indexar en la columna de fecha.
Usr
@ usr Es una combinación PK simple que se ejecuta en menos de un segundo. Apuesta bastante segura aquí.
paparazzo
44
Al forzar una unión hash, estás forzando un escaneo de la tabla grande. Hay mejores opciones
Rob Farley
30

Dado que obtiene el plan correcto con el ORDER BY, ¿tal vez podría simplemente rodar su propio TOPoperador?

SELECT DOCUMENT_ID, COPIES, REQUESTOR, D_ID, FILE_NUMBER
FROM (
    SELECT dc.DOCUMENT_ID,
           dc.COPIES,
           dc.REQUESTOR,
           dc.D_ID,
           cj.FILE_NUMBER,
           ROW_NUMBER() OVER (ORDER BY cj.FILE_NUMBER) AS _rownum
    FROM DOCUMENT_QUEUE dc
    INNER JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
    WHERE dc.QUEUE_DATE <= GETDATE()
      AND dc.PRINT_LOCATION = 2
) AS sub
WHERE _rownum=1;

En mi opinión, el plan de consulta para lo ROW_NUMBER()anterior debería ser el mismo que si tuviera un ORDER BY. El plan de consulta ahora debe tener un Segmento, Proyecto de secuencia y, finalmente, un operador de Filtro, el resto debe parecerse a su buen plan.

Daniel Hutmacher
fuente
3
En realidad, si bien le dio al operador superior (y un montón de otras cosas (un proyecto de secuencia, segmento y clasificación)) todavía se ejecutó en un segundo. Sin embargo, voy a dar la respuesta correcta a @frisbee ya que la suya fue la primera y es más simple. Gran respuesta sin embargo.
Kenneth Fisher
10
@KennethFisher, la respuesta del frisbee es más simple, pero en la forma en que un mazo clava un clavo de acabado más simplemente que un martillo de encuadre estándar. También conlleva un gran riesgo, especialmente si se deja en su lugar a largo plazo. No usaría sugerencias como esa, excepto en las pruebas o tal vez, tal vez sea una excepción marginal.
Steve Mangiameli
@SteveMangiameli En este caso particular, solo hay una combinación, por lo que varias de las preocupaciones desaparecen. Soy consciente de los riesgos de usar una sugerencia de combinación (o sugerencia de consulta). Creo que está justificado en este caso.
Kenneth Fisher
55
@KennethFisher Imo, el principal riesgo de sugerencias de consulta es que a medida que sus datos crecen o cambian, el plan de consultas que aplica puede ser peor de lo que el sistema habría encontrado por sí solo. Ya ha visto cómo un pequeño error en el plan puede afectar seriamente el rendimiento. Usar una pista en producción es declarar: "Sé que este plan siempre será el mejor porque entiendo completamente al planificador y cómo se comportarán mis datos durante la vida útil de esta consulta en producción". Nunca he tenido tanta confianza en una consulta.
jpmc26
29

Editar: +1 funciona en esta situación porque resulta que FILE_NUMBERes una versión de cadena con relleno de cero de un entero. Una mejor solución aquí para las cadenas es agregar ''(la cadena vacía), ya que agregar un valor puede afectar el orden, o para que los números agreguen algo que es una constante pero que contiene una función no determinista, como sign(rand()+1). La idea de 'romper el género' todavía es válida aquí, es solo que mi método no era ideal.

+1

No, no quiero decir que estoy de acuerdo con nada, lo digo como una solución. Si cambia su consulta a, ORDER BY cj.FILE_NUMBER + 1entonces TOP 1se comportará de manera diferente.

Verá, con el objetivo de fila pequeña establecido para una consulta ordenada, el sistema intentará consumir los datos en orden, para evitar tener un operador Ordenar. También evitará construir una tabla hash, ya que probablemente no tenga que trabajar demasiado para encontrar esa primera fila. En su caso, esto está mal: por el grosor de esas flechas, parece que tiene que consumir muchos datos para encontrar una sola coincidencia.

El grosor de esas flechas sugiere que su DOCUMENT_QUEUE tabla (DQ) es mucho más pequeña que su CORRESPONDENCE_JOURNALtabla (CJ). Y que el mejor plan sería verificar las filas DQ hasta encontrar una fila CJ. De hecho, eso es lo que haría el Optimizador de consultas (QO) si no tuviera este molesto ORDER BY, eso está bien respaldado por un índice de cobertura en CJ.

Entonces, si dejaste caer el ORDER BY completo, espero que obtenga un plan que involucre un bucle anidado, iterando sobre las filas en DQ, buscando en CJ para asegurarse de que exista la fila. Y con TOP 1esto, esto se detendría después de que una sola fila fuera retirada.

Pero si realmente necesitas la primera fila en FILE_NUMBER orden, entonces podría engañar al sistema para que ignore ese índice que parece (incorrectamente) ser muy útil al hacerlo ORDER BY CJ.FILE_NUMBER+1, lo que sabemos mantendrá el mismo orden que antes, pero lo más importante es que el QO no lo hace El QO se centrará en obtener todo el conjunto, para que un operador Top N Sort pueda estar satisfecho. Este método debe producir un plan que contenga un operador Compute Scalar para calcular el valor para ordenar y un operador Top N Sort para obtener la primera fila. Pero a la derecha de estos, debería ver un bonito Nested Loop, haciendo muchas búsquedas en CJ. Y un mejor rendimiento que ejecutar a través de una gran tabla de filas que no coinciden con nada en DQ.

El Hash Match no es necesariamente horrible, pero si el conjunto de filas que está devolviendo de DQ es mucho más pequeño que CJ (como yo esperaría que fuera), entonces el Hash Match escaneará mucho más CJ de lo que necesita

Nota: Utilicé +1 en lugar de +0 porque es probable que el optimizador de consultas reconozca que +0 no cambia nada. Por supuesto, lo mismo podría aplicarse al +1, si no ahora, en algún momento en el futuro.

Rob Farley
fuente
7

He leído los resultados de esta publicación y entiendo el concepto de un objetivo de fila, etc. Lo que me interesa es cómo puedo cambiar la consulta para que utilice el mejor plan.

Agregar OPTION (QUERYTRACEON 4138)desactiva el efecto de los objetivos de fila solo para esa consulta, sin ser demasiado prescriptivo sobre el plan final, y probablemente será la forma más simple / directa.

Si agregar esta sugerencia le da un error de permisos (requerido para DBCC TRACEON), puede aplicarlo usando una guía del plan:

Uso QUERYTRACEONen guías de plan por spaghettidba

... o simplemente use un procedimiento almacenado:

¿Qué permisos QUERYTRACEONnecesita? por Kendra Little

Martin Smith
fuente
3

Las versiones más recientes de SQL Server ofrecen opciones diferentes (y posiblemente mejores) para tratar consultas que obtienen un rendimiento subóptimo cuando el optimizador puede aplicar optimizaciones de objetivos de fila. SQL Server 2016 SP1 introdujo el DISABLE_OPTIMIZER_ROWGOAL USE HINTque tiene el mismo efecto que el indicador de seguimiento 4138. Si no está en esa versión, también puede considerar usar la OPTIMIZE FORsugerencia de consulta para obtener un plan de consulta diseñado para devolver todas las filas en lugar de solo 1. La consulta a continuación devolverá los mismos resultados que el de la pregunta, pero no se creará con el objetivo de obtener solo 1 fila.

DECLARE @top INT = 1;

SELECT TOP (@top) dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER
OPTION (OPTIMIZE FOR (@top = 987654321));
Joe Obbish
fuente
2

Como estás haciendo un TOP(1), te recomiendo hacer el ORDER BYdeterminista para empezar. Como mínimo, esto garantizará que los resultados sean funcionalmente predecibles (siempre útiles para las pruebas de regresión). Parece que necesitas agregar DC.D_IDy CJ.CORRESPONDENCE_IDpara eso.

Cuando miro los planes de consulta, a veces me parece instructivo simplificar la consulta: posiblemente seleccione todas las filas de CC relevantes en una tabla temporal de antemano, para eliminar problemas con la estimación de cardinalidad QUEUE_DATEy PRINT_LOCATION. Esto debería ser rápido dado el bajo recuento de filas. A continuación, puede agregar índices a esta tabla temporal si es necesario sin alterar la tabla permanente.

Simon Birch
fuente