¿Por qué una consulta agregada es significativamente más rápida con una cláusula GROUP BY que sin una?

12

Tengo curiosidad por saber por qué una consulta agregada se ejecuta mucho más rápido con una GROUP BYcláusula que sin una.

Por ejemplo, esta consulta tarda casi 10 segundos en ejecutarse

SELECT MIN(CreatedDate)
FROM MyTable
WHERE SomeIndexedValue = 1

Si bien este toma menos de un segundo

SELECT MIN(CreatedDate)
FROM MyTable
WHERE SomeIndexedValue = 1
GROUP BY CreatedDate

Solo hay uno CreatedDateen este caso, por lo que la consulta agrupada devuelve los mismos resultados que la desagrupada.

Noté que los planes de ejecución para las dos consultas son diferentes: la segunda consulta usa Paralelismo mientras que la primera consulta no.

Consulta1 Plan de ejecución Plan de ejecución de Query2

¿Es normal que el servidor SQL evalúe una consulta agregada de manera diferente si no tiene una cláusula GROUP BY? ¿Y hay algo que pueda hacer para mejorar el rendimiento de la primera consulta sin usar una GROUP BYcláusula?

Editar

Acabo de enterarme de que puedo usar OPTION(querytraceon 8649)para establecer el costo general del paralelismo en 0, lo que hace que la consulta use algo de paralelismo y reduce el tiempo de ejecución a 2 segundos, aunque no sé si hay inconvenientes al usar esta sugerencia de consulta.

SELECT MIN(CreatedDate)
FROM MyTable
WHERE SomeIndexedValue = 1
OPTION(querytraceon 8649)

ingrese la descripción de la imagen aquí

Todavía prefiero un tiempo de ejecución más corto ya que la consulta está destinada a completar un valor tras la selección del usuario, por lo que idealmente debería ser instantáneo como lo es la consulta agrupada. En este momento solo estoy terminando mi consulta, pero sé que esa no es realmente una solución ideal.

SELECT Min(CreatedDate)
FROM
(
    SELECT Min(CreatedDate) as CreatedDate
    FROM MyTable WITH (NOLOCK) 
    WHERE SomeIndexedValue = 1
    GROUP BY CreatedDate
) as T

Editar # 2

En respuesta a la solicitud de Martin para más información :

Ambos CreatedDatey SomeIndexedValuetienen un índice separado, no único y no agrupado en ellos. SomeIndexedValuees en realidad un campo varchar (7), aunque almacena un valor numérico que apunta al PK (int) de otra tabla. La relación entre las dos tablas no está definida en la base de datos. Se supone que no debo cambiar la base de datos, y solo puedo escribir consultas que consulten datos.

MyTablecontiene más de 3 millones de registros, y a cada registro se le asigna un grupo al que pertenece ( SomeIndexedValue). Los grupos pueden estar entre 1 y 200,000 registros

Rachel
fuente

Respuestas:

8

Parece que probablemente está siguiendo un índice CreatedDateen orden de menor a mayor y haciendo búsquedas para evaluar el SomeIndexedValue = 1predicado.

Cuando encuentra la primera fila coincidente, está lista, pero puede estar haciendo muchas más búsquedas de las que espera antes de encontrar dicha fila (se supone que las filas que coinciden con el predicado se distribuyen aleatoriamente según la fecha).

Vea mi respuesta aquí para un problema similar

El índice ideal para esta consulta sería uno en SomeIndexedValue, CreatedDate. Suponiendo que no puede agregar eso o al menos hacer que su índice existente en la SomeIndexedValueportada CreatedDatecomo una columna incluida, puede intentar reescribir la consulta de la siguiente manera

SELECT MIN(DATEADD(DAY, 0, CreatedDate)) AS CreatedDate
FROM MyTable
WHERE SomeIndexedValue = 1

para evitar que use ese plan en particular.

Martin Smith
fuente
2

¿Podemos controlar MAXDOP y elegir una tabla conocida, por ejemplo, AdventureWorks.Production.TransactionHistory?

Cuando repito tu configuración usando

--#1
SELECT MIN(TransactionDate) 
FROM AdventureWorks.Production.TransactionHistory
WHERE TransactionID = 100001 
OPTION( MAXDOP 1) ;

--#2
SELECT MIN(TransactionDate) 
FROM AdventureWorks.Production.TransactionHistory
WHERE TransactionID = 100001 
GROUP BY TransactionDate
OPTION( MAXDOP 1) ;
GO 

Los costos son idénticos.

Como comentario aparte, esperaría (lo haría posible) una búsqueda de índice en su valor indexado; de lo contrario, es probable que vea coincidencias hash en lugar de agregados de flujo. Puede mejorar el rendimiento con índices no agrupados que incluyen los valores que está agregando o crear una vista indizada que defina sus agregados como columnas. Entonces estaría golpeando un índice agrupado, que contiene sus agregaciones, por un ID indexado. En SQL Standard, puede crear la vista y usar la sugerencia WITH (NOEXPAND).

Un ejemplo (no uso MIN, ya que no funciona en vistas indexadas):

USE AdventureWorks ;
GO

-- Covering Index with Include
CREATE INDEX IX_CoverAndInclude
ON Production.TransactionHistory(TransactionDate) 
INCLUDE (Quantity) ;
GO

-- Indexed View
CREATE VIEW dbo.SumofQtyByTransDate
    WITH SCHEMABINDING
AS
SELECT 
      TransactionDate 
    , COUNT_BIG(*) AS NumberOfTransactions
    , SUM(Quantity) AS TotalTransactions
FROM Production.TransactionHistory
GROUP BY TransactionDate ;
GO

CREATE UNIQUE CLUSTERED INDEX SumofAllChargesIndex 
    ON dbo.SumofQtyByTransDate (TransactionDate) ;  
GO


--#1
SELECT SUM(Quantity) 
FROM AdventureWorks.Production.TransactionHistory 
WITH (INDEX(0))
WHERE TransactionID = 100001 
OPTION( MAXDOP 1) ;

--#2
SELECT SUM(Quantity)  
FROM AdventureWorks.Production.TransactionHistory 
WITH (INDEX(IX_CoverAndInclude))
WHERE TransactionID = 100001 
GROUP BY TransactionDate
OPTION( MAXDOP 1) ;
GO 

--#3
SELECT SUM(Quantity)  
FROM AdventureWorks.Production.TransactionHistory
WHERE TransactionID = 100001 
GROUP BY TransactionDate
OPTION( MAXDOP 1) ;
GO
fuera de línea
fuente
MAXDOPestablece el grado máximo de paralelismo, lo que limita el número de procesadores que la consulta puede usar. Básicamente, esto haría que la segunda consulta se ejecute tan lenta como la primera, ya que está eliminando sus capacidades para usar paralelismo, que no es lo que quiero.
Rachel
@ Rachel estoy de acuerdo; pero no podemos comparar nada a menos que establezcamos algunas reglas básicas. No puedo comparar fácilmente un proceso paralelo que se ejecuta en 64 núcleos con un solo subproceso que se ejecuta en uno. Al final, espero que todas nuestras máquinas tengan al menos una CPU lógica = -)
ooutwire
0

En mi opinión, la razón del problema es que el optimizador del servidor sql no está buscando el MEJOR plan, sino que busca un buen plan, como es evidente por el hecho de que después de forzar el paralelismo, la consulta se ejecutó mucho más rápido, algo que el optimizador tenía No hecho por sí mismo.

También he visto muchas situaciones en las que reescribir la consulta en un formato diferente era la diferencia entre la paralelización (por ejemplo, aunque la mayoría de los artículos sobre SQL recomiendan la parametrización, he encontrado que a veces no hay paralelización, incluso cuando los parámetros inhalados son los mismos - una paralelizada, o combinar dos consultas con UNION ALL a veces puede eliminar la paralelización).

Como tal, la solución correcta podría ser probar diferentes formas de escribir la consulta, como probar tablas temporales, variables de tabla, cte, tablas derivadas, parametrización, etc., y también jugar con los índices, vistas indexadas o índices filtrados en Para obtener el mejor plan.

Yoel Halb
fuente