Cambiar consulta para mejorar las estimaciones del operador


14

Tengo una consulta que se ejecuta en un período de tiempo aceptable pero quiero obtener el máximo rendimiento posible.

La operación que estoy tratando de mejorar es la "Búsqueda de índice" a la derecha del plan, del Nodo 17.

ingrese la descripción de la imagen aquí

He agregado índices apropiados, pero las estimaciones que obtengo para esa operación son la mitad de lo que se supone que son.

He buscado cambiar mis índices y agregar una tabla temporal y volver a escribir la consulta, pero no pude simplificarla más que esto para obtener las estimaciones correctas.

¿Alguien tiene alguna sugerencia sobre qué más puedo probar?

El plan completo y sus detalles se pueden encontrar aquí .

El plan no anónimo se puede encontrar aquí.

Actualizar:

Tengo la sensación de que la versión inicial de la pregunta generó mucha confusión, por lo que agregaré el código original con algunas explicaciones.

create procedure [dbo].[someProcedure] @asType int, @customAttrValIds idlist readonly
as
begin
    set nocount on;

    declare @dist_ca_id int;

    select *
    into #temp
    from @customAttrValIds
        where id is not null;

    select @dist_ca_id = count(distinct CustomAttrID) 
    from CustomAttributeValues c
        inner join #temp a on c.Id = a.id;

    select a.Id
        , a.AssortmentId 
    from Assortments a
        inner join AssortmentCustomAttributeValues acav
            on a.Id = acav.Assortment_Id
        inner join CustomAttributeValues cav 
            on cav.Id = acav.CustomAttributeValue_Id
    where a.AssortmentType = @asType
        and acav.CustomAttributeValue_Id in (select id from #temp)
    group by a.AssortmentId
        , a.Id
    having count(distinct cav.CustomAttrID) = @dist_ca_id
    option(recompile);

end

Respuestas:

  1. ¿Por qué los nombres iniciales impares en el enlace pasteThePlan?

    Respuesta : Porque utilicé el plan de anonimato de SQL Sentry Plan Explorer.

  2. ¿Por qué OPTION RECOMPILE?

    Respuesta : Porque puedo permitirme recompilar para evitar la detección de parámetros (los datos están / podrían estar sesgados). Lo he probado y estoy contento con el plan que genera el Optimizer mientras lo uso OPTION RECOMPILE.

  3. WITH SCHEMABINDING?

    Respuesta : Realmente quiero evitar eso y lo usaría solo cuando tengo una vista indizada. De todos modos, esta es una función del sistema ( COUNT()), así que no sirve de nada SCHEMABINDINGaquí.

Respuestas a más preguntas posibles:

  1. ¿Por qué lo uso INSERT INTO #temp FROM @customAttrributeValues?

    Respuesta : Debido a que noté y ahora sé que cuando uso variables conectadas a una consulta, cualquier estimación que salga de trabajar con una variable siempre es 1. Y probé poniendo los datos en una tabla temporal y la Estimada es igual a Filas reales .

  2. ¿Por qué lo usé and acav.CustomAttributeValue_Id in (select id from #temp)?

    Respuesta : Podría haberlo reemplazado con JOIN en #temp, pero los desarrolladores estaban muy confundidos y ofrecieron la INopción. Realmente no creo que haya una diferencia incluso al reemplazar y de cualquier manera, no hay problema con esto.


Supongo que la #tempcreació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 cambiarlo in (select id from #temp)a una existssubconsulta.)
ypercubeᵀᴹ

@ ypercubeᵀᴹ Cierto, se leen unas pocas páginas menos con el uso de la variable en lugar de una tabla temporal.
Radu Gheorghiu

Por cierto, una variable de tabla proporcionará la estimación de recuento de filas correcta cuando se utiliza con la opción (recompilación) - pero todavía no tienen estadísticas granulares, etc. cardinalidad
TH

@TH Bueno, busqué en el plan de ejecución real las estimaciones, cuando utilicé en select id from @customAttrValIdslugar de select id from #tempy el número estimado de filas era 1para la variable y 3para #temp (que coincidía con el número real de filas). Es por eso que reemplacé @con #. Y HAGO recordar una charla (de Brent O o Aaron Bertrand) donde dijeron que cuando se utiliza una variable TBL las estimaciones para que siempre será 1. Y como una mejora para obtener mejores estimaciones que utilizarían una tabla temporal.
Radu Gheorghiu

@RaduGheorghiu Sí, pero en el mundo de esos tipos, la opción (recompilar) rara vez es una opción, y también prefieren tablas temporales por otras razones válidas. Tal vez la estimación simplemente siempre se muestra incorrectamente como 1, ya que cambia el plan como se ve aquí: theboreddba.com/Categories/FunWithFlags/…
TH

Respuestas:


12

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:

Nodo 8 Hash Match (Agregado)

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 #temptabla 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 #TempTabledespué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 #Temptabla 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 KEYagrega 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 #Temp2deberí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):

# Temp2 población

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:

Plan final

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.


9

Las estimaciones de cardinalidad en su consulta son realmente muy buenas. Es raro obtener el número de filas estimadas para que coincida exactamente con el número de filas reales, especialmente cuando tiene tantas uniones. Unir las estimaciones de cardinalidad es complicado para que el optimizador funcione correctamente. Una cosa importante a tener en cuenta es que el número de filas estimadas para la parte interna del bucle anidado es por ejecución de ese bucle. Entonces, cuando SQL Server dice que se buscarán 463869 filas con el índice, la estimación real en este caso es el número de ejecuciones (2) * 463869 = 927738, que no está muy lejos del número real de filas, 1391608. Sorprendentemente, el número de filas estimadas es casi perfecto inmediatamente después de la unión del bucle anidado en el ID de nodo 10.

Las estimaciones de baja cardinalidad son principalmente un problema cuando el optimizador de consultas elige el plan incorrecto o no otorga suficiente memoria al plan. No veo ningún derrame en tempdb para este plan, por lo que la memoria se ve bien. Para la unión de bucle anidado que llama, tiene una pequeña tabla externa y una tabla interna indexada. ¿Qué está mal con eso? Para ser precisos, ¿qué esperaría que el optimizador de consultas haga de manera diferente aquí?

En términos de mejorar el rendimiento, lo que me llama la atención es que SQL Server está utilizando un algoritmo de hash para distribuir filas paralelas, lo que hace que todos estén en el mismo hilo:

desequilibrio de hilo

Como resultado, un hilo hace todo el trabajo con la búsqueda de índice:

búsqueda de desequilibrio de hilo

Eso significa que su consulta no se ejecuta en paralelo de manera efectiva hasta que el operador de secuencias de repartición en el id de nodo 9. Lo que probablemente desee es la partición round robin para que cada fila termine en su propio hilo. Eso permitirá que dos subprocesos realicen la búsqueda del índice para la ID de nodo 17. Agregar un TOPoperador superfluo puede obtener una partición de turnos. Puedo agregar detalles aquí si lo desea.

Si realmente desea centrarse en las estimaciones de cardinalidad, puede colocar las filas después de la primera unión en una tabla temporal. Si reúne estadísticas en la tabla temporal que le da al optimizador más información sobre la tabla externa para la unión de bucle anidado que llamó. También podría dar lugar a particiones round robin.

Si no está utilizando las marcas de seguimiento 4199 o 2301, podría considerarlas. La marca de seguimiento 4199 ofrece una amplia variedad de soluciones de optimización, pero pueden degradar algunas cargas de trabajo. La marca de seguimiento 2301 cambia algunos de los supuestos de cardinalidad de unión del optimizador de consultas y hace que trabaje más. En ambos casos, pruebe cuidadosamente antes de habilitarlos.


-2

Creo que obtener una mejor estimación de esa unión no cambiará el plan, a menos que 1.4 mill sea una porción suficiente de la tabla para hacer que el optimizador elija un escaneo de índice (no clúster) con hash o combinación de fusión. Sospecho que no sería el caso aquí, ni realmente útil, pero puede probar los efectos reemplazando unión interna por CustomAttributeValues ​​con unión hash interna y unión de fusión interna .

También he examinado el código de manera más amplia y no veo ninguna forma de mejorarlo; por supuesto, me interesaría que me demuestren lo contrario. Y si desea publicar la lógica completa de lo que está tratando de lograr, me interesaría otra mirada.


3
Hay un gran espacio de planes para esa consulta, con muchas opciones para unir orden y anidamiento, paralelismo, agregación local / global, etc., la mayoría de los cuales se verían afectados por cambios en las estadísticas derivadas (distribución y cardinalidad sin procesar) en el nodo 10 del plan. Tenga en cuenta también que las sugerencias de unión generalmente deben evitarse ya que vienen con un silencio OPTION(FORCE ORDER), lo que evita que el optimizador reordene las uniones desde la secuencia de texto, y muchas otras optimizaciones además.
Paul White 9

-12

No va a mejorar desde una Búsqueda de índice [no agrupada]. Lo único mejor que una búsqueda de índice no agrupado es una Búsqueda de índice agrupado.

Además, he sido un DBA de SQL durante los últimos diez años, y un desarrollador de SQL durante cinco años antes de eso, y en mi experiencia es extremadamente raro encontrar una mejora en una consulta SQL al estudiar el plan de ejecución que no podría ' t encontrar por otros medios. La razón principal para generar el plan de ejecución es porque a menudo le sugerirá índices faltantes que puede agregar para mejorar el rendimiento.

Las principales ganancias de rendimiento estarán en ajustar la consulta SQL en sí, si hay alguna ineficiencia allí. Por ejemplo, hace un par de meses obtuve una función SQL para ejecutarse 160 veces más rápido al reescribir una SELECT UNION SELECTtabla dinámica de estilo para usar el PIVOToperador SQL estándar .

insert into Variable1 values (?), (?), (?)


select *
    into Object1
    from Variable2
        where Column1 is not null;



select Variable3 = Function1(distinct Column2) 
    from Object2 Object3
        inner join Object1 Object4 on Object3.Column1 = Object4.Column1;



select Object4.Column1
        , Object4.Column3 
    from Object5 Object4
        inner join Object6 Object7
            on Object4.Column1 = Object7.Column4
        inner join Object2 Object8 
            on Object8.Column1 = Object7.Column5
    where Object4.Column6 = Variable4
        and Object7.Column5 in (select Column1 from Object1)
    group by Object4.Column3
        , Object4.Column1
    having Function1(distinct Object8.Column2) = Variable3
    option(recompile);

Entonces, veamos, SELECT * INTOgeneralmente es menos eficiente que un estándar INSERT Object1 (column list) SELECT column list. Entonces reescribiría eso. A continuación, si Function1 se definió sin a WITH SCHEMABINDING, agregar una WITH SCHEMABINDINGcláusula debería permitir que se ejecute más rápido.

Has elegido muchos alias que no tienen sentido, como alias Object2 como Object3. Debe elegir mejores alias que no ofusquen el código. Tiene "Object7.Column5 en (seleccione Column1 de Object1)".

INLas cláusulas de esta naturaleza son siempre más eficientes escritas como EXISTS (SELECT 1 FROM Object1 o1 WHERE o1.Column1 = Object7.Column5). Quizás debería haber escrito eso de la otra manera. EXISTSsiempre será al menos tan bueno como IN. No siempre es mejor, pero generalmente lo es.

Además, dudo que option(recompile)esté mejorando el rendimiento de la consulta aquí. Yo probaría quitándolo.


66
Si una búsqueda de índice no agrupado cubre la consulta, casi siempre será mejor que una búsqueda de índice agrupado, porque, por definición, el índice agrupado tiene todas las columnas y el índice no agrupado tiene menos columnas, por lo que requerirá menos búsquedas de página (y menos niveles de pasos en el b-tree) para recuperar los datos. Por lo tanto, no es exacto decir que una búsqueda de índice agrupado siempre será mejor.
ErikE
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.