El plan se compiló en una instancia de SQL Server 2008 R2 RTM (compilación 10.50.1600). Debe instalar el Service Pack 3 (compilación 10.50.6000), seguido de los últimos parches para actualizarlo a la última compilación (actual) 10.50.6542. Esto es importante por varias razones, incluidas la seguridad, la corrección de errores y las nuevas funciones.
La optimización de incrustación de parámetros
Relevante para la presente pregunta, SQL Server 2008 R2 RTM no admite la optimización de incrustación de parámetros (PEO) para OPTION (RECOMPILE)
. En este momento, está pagando el costo de las recompilaciones sin darse cuenta de uno de los principales beneficios.
Cuando PEO está disponible, SQL Server puede usar los valores literales almacenados en variables y parámetros locales directamente en el plan de consulta. Esto puede conducir a simplificaciones dramáticas y aumentos de rendimiento. Hay más información sobre eso en mi artículo, Parámetro Sniffing, incrustación y las opciones RECOMPILE .
Hash, ordenar e intercambiar derrames
Estos solo se muestran en los planes de ejecución cuando la consulta se compiló en SQL Server 2012 o posterior. En versiones anteriores, teníamos que monitorear los derrames mientras la consulta se ejecutaba con Profiler o Extended Events. Los derrames siempre resultan en E / S física hacia (y desde) el tempdb de respaldo de almacenamiento persistente , que puede tener importantes consecuencias de rendimiento, especialmente si el derrame es grande o la ruta de E / S está bajo presión.
En su plan de ejecución, hay dos operadores Hash Match (Agregados). La memoria reservada para la tabla hash se basa en la estimación de las filas de salida (en otras palabras, es proporcional al número de grupos encontrados en tiempo de ejecución). La memoria otorgada se repara justo antes de que comience la ejecución, y no puede crecer durante la ejecución, independientemente de la cantidad de memoria libre que tenga la instancia. En el plan suministrado, ambos operadores de Hash Match (Agregado) producen más filas de las esperadas por el optimizador y, por lo tanto, pueden experimentar un derrame a tempdb en tiempo de ejecución.
También hay un operador Hash Match (Inner Join) en el plan. La memoria reservada para la tabla hash se basa en la estimación de las filas de entrada del lado de la sonda . La entrada de la sonda estima 847,399 filas, pero 1,223,636 se encuentran en tiempo de ejecución. Este exceso también puede estar causando un derrame de picadillo.
Agregado redundante
Hash Match (Aggregate) en el nodo 8 realiza una operación de agrupación (Assortment_Id, CustomAttrID)
, pero las filas de entrada son iguales a las filas de salida:
Esto sugiere que la combinación de columnas es una clave (por lo que la agrupación es semánticamente innecesaria). El costo de realizar el agregado redundante se incrementa por la necesidad de pasar los 1.4 millones de filas dos veces a través de intercambios de particionamiento hash (los operadores de Paralelismo a ambos lados).
Dado que las columnas involucradas provienen de diferentes tablas, es más difícil de lo habitual comunicar esta información de singularidad al optimizador, por lo que puede evitar la operación de agrupación redundante y los intercambios innecesarios.
Distribución de hilo ineficiente
Como se señaló en la respuesta de Joe Obbish , el intercambio en el nodo 14 usa la partición hash para distribuir filas entre hilos. Desafortunadamente, el pequeño número de filas y los planificadores disponibles significa que las tres filas terminan en un solo hilo. El plan aparentemente paralelo se ejecuta en serie (con sobrecarga paralela) hasta el intercambio en el nodo 9.
Puede abordar esto (para obtener particiones de round-robin o broadcast) eliminando la ordenación distinta en el nodo 13. La forma más fácil de hacerlo es crear una clave primaria agrupada en la #temp
tabla y realizar la operación distinta al cargar la tabla:
CREATE TABLE #Temp
(
id integer NOT NULL PRIMARY KEY CLUSTERED
);
INSERT #Temp
(
id
)
SELECT DISTINCT
CAV.id
FROM @customAttrValIds AS CAV
WHERE
CAV.id IS NOT NULL;
Almacenamiento temporal de estadísticas en la tabla
A pesar del uso de OPTION (RECOMPILE)
, SQL Server aún puede almacenar en caché el objeto de tabla temporal y sus estadísticas asociadas entre llamadas de procedimiento. Esto generalmente es una optimización de rendimiento bienvenida, pero si la tabla temporal se llena con una cantidad similar de datos en llamadas a procedimientos adyacentes, el plan recompilado puede basarse en estadísticas incorrectas (almacenadas en caché de una ejecución anterior). Esto se detalla en mis artículos, Tablas temporales en procedimientos almacenados y Almacenamiento en caché temporal de tablas explicado .
Para evitar esto, úselo OPTION (RECOMPILE)
junto con un explícito UPDATE STATISTICS #TempTable
después de que se complete la tabla temporal y antes de que se haga referencia en una consulta.
Consulta reescribir
Esta parte supone que los cambios en la creación de la #Temp
tabla ya se han realizado.
Dados los costos de posibles derrames de hash y el agregado redundante (y los intercambios circundantes), puede pagar materializar el conjunto en el nodo 10:
CREATE TABLE #Temp2
(
CustomAttrID integer NOT NULL,
Assortment_Id integer NOT NULL,
);
INSERT #Temp2
(
Assortment_Id,
CustomAttrID
)
SELECT
ACAV.Assortment_Id,
CAV.CustomAttrID
FROM #temp AS T
JOIN dbo.CustomAttributeValues AS CAV
ON CAV.Id = T.id
JOIN dbo.AssortmentCustomAttributeValues AS ACAV
ON T.id = ACAV.CustomAttributeValue_Id;
ALTER TABLE #Temp2
ADD CONSTRAINT PK_#Temp2_Assortment_Id_CustomAttrID
PRIMARY KEY CLUSTERED (Assortment_Id, CustomAttrID);
Se PRIMARY KEY
agrega en un paso separado para garantizar que la compilación del índice tenga información precisa sobre la cardinalidad y para evitar el problema de almacenamiento en caché de estadísticas de la tabla temporal.
Es probable que esta materialización ocurra en la memoria (evitando tempdb I / O) si la instancia tiene suficiente memoria disponible. Esto es aún más probable una vez que actualice a SQL Server 2012 (SP1 CU10 / SP2 CU1 o posterior), lo que ha mejorado el comportamiento de Eager Write .
Esta acción le da al optimizador información precisa sobre la cardinalidad en el conjunto intermedio, le permite crear estadísticas y nos permite declarar (Assortment_Id, CustomAttrID)
como clave.
El plan para la población de #Temp2
debería verse así (tenga en cuenta el análisis de índice agrupado de #Temp
, sin clasificación distinta, y el intercambio ahora utiliza la división de filas round-robin):
Con ese conjunto disponible, la consulta final se convierte en:
SELECT
A.Id,
A.AssortmentId
FROM
(
SELECT
T.Assortment_Id
FROM #Temp2 AS T
GROUP BY
T.Assortment_Id
HAVING
COUNT_BIG(DISTINCT T.CustomAttrID) = @dist_ca_id
) AS DT
JOIN dbo.Assortments AS A
ON A.Id = DT.Assortment_Id
WHERE
A.AssortmentType = @asType
OPTION (RECOMPILE);
Podríamos reescribir manualmente COUNT_BIG(DISTINCT...
como simple COUNT_BIG(*)
, pero con la nueva información clave, el optimizador lo hace por nosotros:
El plan final puede usar una unión loop / hash / merge dependiendo de la información estadística sobre los datos a los que no tengo acceso. Otra pequeña nota: supuse que CREATE [UNIQUE?] NONCLUSTERED INDEX IX_ ON dbo.Assortments (AssortmentType, Id, AssortmentId);
existe un índice como .
De todos modos, lo importante sobre los planes finales es que las estimaciones deberían ser mucho mejores, y la compleja secuencia de operaciones de agrupación se ha reducido a un solo Agregado de flujo (que no requiere memoria y, por lo tanto, no puede derramarse en el disco).
Es difícil decir que el rendimiento en realidad será mejor en este caso con la tabla temporal adicional, pero las estimaciones y las opciones de plan serán mucho más resistentes a los cambios en el volumen y la distribución de datos a lo largo del tiempo. Eso puede ser más valioso a largo plazo que un pequeño aumento de rendimiento en la actualidad. En cualquier caso, ahora tiene mucha más información sobre la cual basar su decisión final.
#temp
creación y el uso serían un problema para el rendimiento, no una ganancia. Está guardando en una tabla no indexada solo para usarla una vez. Intente eliminarlo por completo (y posiblemente cambiarloin (select id from #temp)
a unaexists
subconsulta.)