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.
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:
¿Por qué los nombres iniciales impares en el enlace pasteThePlan?
Respuesta : Porque utilicé el plan de anonimato de SQL Sentry Plan Explorer.
¿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
.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 nadaSCHEMABINDING
aquí.
Respuestas a más preguntas posibles:
¿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 .
¿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
IN
opción. Realmente no creo que haya una diferencia incluso al reemplazar y de cualquier manera, no hay problema con esto.
fuente
#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.)select id from @customAttrValIds
lugar deselect id from #temp
y el número estimado de filas era1
para la variable y3
para #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.Respuestas:
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: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ícitoUPDATE 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:
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:
Podríamos reescribir manualmente
COUNT_BIG(DISTINCT...
como simpleCOUNT_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.
fuente
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:
Como resultado, un hilo hace todo el trabajo con la búsqueda de índice:
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
TOP
operador 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.
fuente
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.
fuente
OPTION(FORCE ORDER)
, lo que evita que el optimizador reordene las uniones desde la secuencia de texto, y muchas otras optimizaciones además.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 SELECT
tabla dinámica de estilo para usar elPIVOT
operador SQL estándar .Entonces, veamos,
SELECT * INTO
generalmente es menos eficiente que un estándarINSERT Object1 (column list) SELECT column list
. Entonces reescribiría eso. A continuación, si Function1 se definió sin aWITH SCHEMABINDING
, agregar unaWITH SCHEMABINDING
clá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)".
IN
Las cláusulas de esta naturaleza son siempre más eficientes escritas comoEXISTS (SELECT 1 FROM Object1 o1 WHERE o1.Column1 = Object7.Column5)
. Quizás debería haber escrito eso de la otra manera.EXISTS
siempre será al menos tan bueno comoIN
. 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.fuente