¿Por qué seleccionar todas las columnas resultantes de esta consulta es más rápido que seleccionar la columna que me interesa?

13

Tengo una consulta donde usar select *no solo hace muchas menos lecturas, sino que también usa significativamente menos tiempo de CPU que usarselect c.Foo .

Esta es la consulta:

select top 1000 c.ID
from ATable a
    join BTable b on b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
    join CTable c on c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
where (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff)
    and b.IsVoided = 0
    and c.ComplianceStatus in (3, 5)
    and c.ShipmentStatus in (1, 5, 6)
order by a.LastAnalyzedDate

Esto terminó con 2,473,658 lecturas lógicas, principalmente en la Tabla B. Usó 26,562 CPU y tuvo una duración de 7,965.

Este es el plan de consulta generado:

Planificar desde la selección del valor de una sola columna En PasteThePlan: https://www.brentozar.com/pastetheplan/?id=BJAp2mQIQ

Cuando me cambio c.IDa* , la consulta terminó con 107.049 lecturas lógicas, distribuidas de manera bastante uniforme entre las tres tablas. Usó 4,266 CPU y tuvo una duración de 1,147.

Este es el plan de consulta generado:

Planificar desde Seleccionar todos los valores En PasteThePlan: https://www.brentozar.com/pastetheplan/?id=SyZYn7QUQ

Intenté usar las sugerencias de consulta sugeridas por Joe Obbish, con estos resultados:
select c.IDsin sugerencia: https://www.brentozar.com/pastetheplan/?id=SJfBdOELm
select c.ID con sugerencia: https://www.brentozar.com/pastetheplan/ ? id = B1W ___ N87
select * sin pista: https://www.brentozar.com/pastetheplan/?id=HJ6qddEIm
select * con pista: https://www.brentozar.com/pastetheplan/?id=rJhhudNIQ

El uso de la OPTION(LOOP JOIN)sugerencia con select c.IDredujo drásticamente el número de lecturas en comparación con la versión sin la sugerencia, pero todavía está haciendo aproximadamente 4 veces el número de lecturas de la select *consulta sin ninguna sugerencia. Agregando OPTION(RECOMPILE, HASH JOIN)a laselect * consulta hizo que funcione mucho peor que cualquier otra cosa que haya intentado.

Después de actualizar las estadísticas en las tablas y sus índices utilizando WITH FULLSCAN, la select c.IDconsulta se ejecuta mucho más rápido:
select c.IDantes de la actualización: https://www.brentozar.com/pastetheplan/?id=SkiYoOEUm
select * antes de la actualización: https://www.brentozar.com/ pastetheplan /? id = ryrvodEUX
select c.ID después de la actualización: https://www.brentozar.com/pastetheplan/?id=B1MRoO487
select * después de la actualización: https://www.brentozar.com/pastetheplan/?id=Hk7si_V8m

select *todavía tiene un rendimiento superior select c.IDen términos de duración total y lecturas totales ( select *tiene aproximadamente la mitad de las lecturas) pero usa más CPU. En general, están mucho más cerca que antes de la actualización, sin embargo, los planes aún difieren.

Se observa el mismo comportamiento en 2016 en el modo de compatibilidad de 2014 y en 2014. ¿Qué podría explicar la disparidad entre los dos planes? ¿Podría ser que no se hayan creado los índices "correctos"? ¿Podrían las estadísticas estar un poco desactualizadas causar esto?

Intenté mover los predicados a la ONparte de la unión, de varias maneras, pero el plan de consulta es el mismo cada vez.

Después de reconstrucciones de índice

Reconstruí todos los índices en las tres tablas involucradas en la consulta. c.IDsigue haciendo la mayor cantidad de lecturas (más del doble *), pero el uso de la CPU es aproximadamente la mitad de la *versión. La c.IDversión también se derramó en tempdb en la clasificación de ATable:
c.ID: https://www.brentozar.com/pastetheplan/?id=HyHIeDO87
* : https://www.brentozar.com/pastetheplan/?id=rJ4deDOIQ

También intenté forzarlo a funcionar sin paralelismo, y eso me dio la mejor consulta: https://www.brentozar.com/pastetheplan/?id=SJn9-vuLX

Noté el recuento de ejecuciones de operadores DESPUÉS de la búsqueda de índice grande que está haciendo que la orden solo se ejecute 1,000 veces en la versión de subproceso único, pero hizo mucho más en la versión paralela, entre 2,622 y 4,315 ejecuciones de varios operadores.

L. Miller
fuente

Respuestas:

4

Es cierto que seleccionar más columnas implica que SQL Server puede necesitar trabajar más para obtener los resultados solicitados de la consulta. Si el optimizador de consultas lograra el plan de consulta perfecto para ambas consultas, entonces sería razonable esperar queSELECT *consulta para ejecutar más tiempo que la consulta que selecciona todas las columnas de todas las tablas. Has observado lo contrario para tu par de consultas. Debe tener cuidado al comparar costos, pero la consulta lenta tiene un costo total estimado de 1090.08 unidades optimizadoras y la consulta rápida tiene un costo total estimado de 6823.11 unidades optimizadoras. En este caso, se podría decir que el optimizador hace un mal trabajo al estimar los costos totales de consulta. Escogió un plan diferente para su consulta SELECT * y esperaba que ese plan fuera más costoso, pero ese no fue el caso aquí. Ese tipo de desajuste puede ocurrir por muchas razones y una de las causas más comunes son los problemas de estimación de cardinalidad. Los costos del operador están determinados en gran medida por las estimaciones de cardinalidad. Si una estimación de cardinalidad en un punto clave de un plan es inexacta, entonces el costo total del plan puede no reflejar la realidad. Esta es una simplificación excesiva, pero espero que sea útil para comprender lo que está sucediendo aquí.

Comencemos discutiendo por qué una SELECT *consulta puede ser más costosa que seleccionar una sola columna. La SELECT *consulta puede convertir algunos índices de cobertura en índices que no sean de cobertura, lo que puede significar que el optimizador necesita hacer un trabajo adicional para obtener todas las columnas que necesita o puede necesitar leer de un índice más grande.SELECT *también puede dar como resultado conjuntos de resultados intermedios más grandes que deben procesarse durante la ejecución de la consulta. Puede ver esto en acción mirando los tamaños de fila estimados en ambas consultas. En la consulta rápida, los tamaños de fila varían de 664 bytes a 3019 bytes. En la consulta lenta, los tamaños de fila varían de 19 a 36 bytes. Los operadores de bloqueo, como los tipos o las compilaciones hash, tendrán mayores costos para los datos con un tamaño de fila más grande porque SQL Server sabe que es más costoso ordenar grandes cantidades de datos o convertirlos en una tabla hash.

En cuanto a la consulta rápida, el optimizador estima que necesita hacer 2.4 millones de búsquedas de índice Database1.Schema1.Object5.Index3. De ahí proviene la mayor parte del costo del plan. Sin embargo, el plan real revela que solo se realizaron 1332 búsquedas de índice en ese operador. Si compara las filas reales con las estimadas para las partes externas de esas uniones de bucle, verá grandes diferencias. El optimizador cree que se necesitarán muchas más búsquedas de índice para encontrar las primeras 1000 filas necesarias para los resultados de la consulta. Es por eso que la consulta tiene un plan de costo relativamente alto pero termina tan rápido: el operador que se predijo que sería el más costoso hizo menos del 0.1% de su trabajo esperado.

Mirando la consulta lenta, obtienes un plan con uniones en su mayoría hash (creo que la unión en bucle está ahí solo para tratar con la variable local). Las estimaciones de cardinalidad definitivamente no son perfectas, pero el único problema de estimación real está justo al final con el tipo. Sospecho que la mayor parte del tiempo se dedica a los escaneos de las tablas con cientos de millones de filas.

Puede resultarle útil agregar sugerencias de consulta a ambas versiones de la consulta para forzar el plan de consulta asociado con la otra versión. Las sugerencias de consulta pueden ser una buena herramienta para descubrir por qué el optimizador hizo algunas de sus elecciones. Si agrega OPTION (RECOMPILE, HASH JOIN)a la SELECT *consulta, espero que vea un plan de consulta similar a la consulta de combinación hash. También espero que los costos de consulta sean mucho más altos para el plan de combinación hash porque los tamaños de fila son mucho más grandes. Entonces, esa podría ser la razón por la que la consulta de combinación hash no se eligió para la SELECT *consulta. Si agrega OPTION (LOOP JOIN)a la consulta que selecciona solo una columna, espero que vea un plan de consulta similar al delSELECT *consulta. En este caso, reducir el tamaño de la fila no debería tener un gran impacto en el costo general de la consulta. Puede omitir las búsquedas clave, pero eso es un pequeño porcentaje del costo estimado.

En resumen, espero que los tamaños de fila más grandes necesarios para satisfacer la SELECT *consulta empujen al optimizador hacia un plan de unión de bucle en lugar de un plan de unión hash. El plan de unión en bucle cuesta más de lo debido debido a problemas de estimación de cardinalidad. Reducir el tamaño de las filas seleccionando solo una columna reduce en gran medida el costo de un plan de unión hash, pero probablemente no tendrá mucho efecto en el costo de un plan de unión en bucle, por lo que terminará con el plan de unión hash menos eficiente. Es difícil decir más que esto para un plan anónimo.

Joe Obbish
fuente
Muchas gracias por su respuesta amplia e informativa. Intenté agregar las sugerencias que sugeriste. Realizó la select c.IDconsulta mucho más rápido, pero todavía está haciendo un trabajo adicional que la select *consulta, sin pistas, hace.
L. Miller
2

Las estadísticas obsoletas ciertamente pueden hacer que el optimizador elija un método deficiente para encontrar los datos. ¿Has intentado hacer un UPDATE STATISTICS ... WITH FULLSCANo haciendo un completo REBUILDen el índice? Intenta eso y mira si ayuda.

ACTUALIZAR

Según una actualización del OP:

Después de actualizar las estadísticas en las tablas y sus índices utilizando WITH FULLSCAN, la select c.IDconsulta se ejecuta mucho más rápido

Entonces, ahora, si la única acción que se tomó fue UPDATE STATISTICS, intente hacer un índice REBUILD(no REORGANIZE), ya que he visto esa ayuda con los recuentos de filas estimados donde ambos UPDATE STATISTICSy el índice REORGANIZEno.

Solomon Rutzky
fuente
Pude obtener todos los índices en las tres tablas involucradas para reconstruir durante el fin de semana, y he actualizado mi publicación para reflejar esos resultados.
L. Miller
-1
  1. ¿Pueden incluir los guiones de índice?
  2. ¿Has eliminado posibles problemas con el "rastreo de parámetros"? https://www.mssqltips.com/sqlservertip/3257/different-approaches-to-correct-sql-server-parameter-sniffing/
  3. He encontrado que esta técnica es útil en algunos casos:
    a) reescribir cada tabla como una subconsulta, siguiendo estas reglas:
    b) SELECCIONAR - colocar columnas de unión primero
    c) PREDICADOS - moverse a sus respectivas subconsultas
    d) ORDENAR POR - moverse a su respectivas subconsultas, ordene en UNIR COLUMNAS PRIMERO
    e) Agregue una consulta de contenedor para su clasificación final y SELECCIONE.

La idea es ordenar previamente las columnas de unión dentro de cada subselección, colocando las columnas de unión primero en cada lista de selección.

Esto es lo que quiero decir ...

SELECT ... wrapper query
FROM
(
    SELECT ...
    FROM
        (SELECT ClientID, ShipKey, NextAnalysisDate
         FROM ATABLE
         WHERE (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff) -- Predicates
         ORDER BY OrderKey, ClientID, LastAnalyzedDate  ---- Pre-sort the join columns
        ) as a
        JOIN 
        (SELECT OrderKey, ClientID, OrderID, IsVoided
         FROM BTABLE
         WHERE IsVoided = 0             ---- Include all predicates
         ORDER BY OrderKey, OrderID, IsVoided       ---- Pre-sort the join columns
        ) as b ON b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
        JOIN
        (SELECT OrderID, ShipKey, ComplianceStatus, ShipmentStatus, ID
         FROM CTABLE
         WHERE ComplianceStatus in (3, 5)       ---- Include all predicates
             AND ShipmentStatus in (1, 5, 6)        ---- Include all predicates
         ORDER BY OrderID, ShipKey          ---- Pre-sort the join columns
        ) as c ON c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
) as d
ORDER BY d.LastAnalyzedDate
Victor Di Leo
fuente
1
1. Intentaré agregar scripts DDL de índice a la publicación original, lo que puede demorar un poco en "eliminarlos". 2. Probé esta posibilidad limpiando la memoria caché del plan antes de ejecutar y reemplazando el parámetro de enlace con un valor real. 3. ORDER BYIntenté esto, pero no es válido en una subconsulta sin TOP, FORXML, etc. Lo intenté sin las ORDER BYcláusulas pero era el mismo plan.
L. Miller