Cómo optimizar la consulta T-SQL usando el Plan de ejecución

15

Tengo una consulta SQL que he pasado los últimos dos días tratando de optimizar usando prueba y error y el plan de ejecución, pero fue en vano. Perdóname por hacer esto, pero publicaré todo el plan de ejecución aquí. He hecho el esfuerzo de hacer que los nombres de tabla y columna en el plan de consulta y ejecución sean genéricos tanto por brevedad como para proteger la propiedad intelectual de mi empresa. El plan de ejecución se puede abrir con SQL Sentry Plan Explorer .

He hecho una buena cantidad de T-SQL, pero el uso de planes de ejecución para optimizar mi consulta es un área nueva para mí y realmente he tratado de entender cómo hacerlo. Por lo tanto, si alguien pudiera ayudarme con esto y explicar cómo se puede descifrar este plan de ejecución para encontrar formas en la consulta para optimizarlo, estaría eternamente agradecido. Tengo muchas más consultas para optimizar, solo necesito un trampolín para ayudarme con este primero.

Esta es la consulta:

DECLARE @Param0 DATETIME     = '2013-07-29';
DECLARE @Param1 INT          = CONVERT(INT, CONVERT(VARCHAR, @Param0, 112))
DECLARE @Param2 VARCHAR(50)  = 'ABC';
DECLARE @Param3 VARCHAR(100) = 'DEF';
DECLARE @Param4 VARCHAR(50)  = 'XYZ';
DECLARE @Param5 VARCHAR(100) = NULL;
DECLARE @Param6 VARCHAR(50)  = 'Text3';

SET NOCOUNT ON

DECLARE @MyTableVar TABLE
(
    B_Var1_PK int,
    Job_Var1 varchar(512),
    Job_Var2 varchar(50)
)

INSERT INTO @MyTableVar (B_Var1_PK, Job_Var1, Job_Var2) 
SELECT B_Var1_PK, Job_Var1, Job_Var2 FROM [fn_GetJobs] (@Param1, @Param2, @Param3, @Param4, @Param6);

CREATE TABLE #TempTable
(
    TTVar1_PK INT PRIMARY KEY,
    TTVar2_LK VARCHAR(100),
    TTVar3_LK VARCHAR(50),
    TTVar4_LK INT,
    TTVar5 VARCHAR(20)
);

INSERT INTO #TempTable
SELECT DISTINCT
    T.T1_PK,
    T.T1_Var1_LK,
    T.T1_Var2_LK,
    MAX(T.T1_Var3_LK),
    T.T1_Var4_LK
FROM
    MyTable1 T
    INNER JOIN feeds.MyTable2 A ON A.T2_Var1 = T.T1_Var4_LK
    INNER JOIN @MyTableVar B ON B.Job_Var2 = A.T2_Var2 AND B.Job_Var1 = A.T2_Var3
GROUP BY T.T1_PK, T.T1_Var1_LK, T.T1_Var2_LK, T.T1_Var4_LK

-- This is the slow statement...
SELECT 
    CASE E.E_Var1_LK
        WHEN 'Text1' THEN T.TTVar2_LK + '_' + F.F_Var1
        WHEN 'Text2' THEN T.TTVar2_LK + '_' + F.F_Var2
        WHEN 'Text3' THEN T.TTVar2_LK
    END,
    T.TTVar4_LK,
    T.TTVar3_LK,
    CASE E.E_Var1_LK
        WHEN 'Text1' THEN F.F_Var1
        WHEN 'Text2' THEN F.F_Var2
        WHEN 'Text3' THEN T.TTVar5
    END,
    A.A_Var3_FK_LK,
    C.C_Var1_PK,
    SUM(CONVERT(DECIMAL(18,4), A.A_Var1) + CONVERT(DECIMAL(18,4), A.A_Var2))
FROM #TempTable T
    INNER JOIN TableA (NOLOCK) A ON A.A_Var4_FK_LK  = T.TTVar1_PK
    INNER JOIN @MyTableVar     B ON B.B_Var1_PK     = A.Job
    INNER JOIN TableC (NOLOCK) C ON C.C_Var2_PK     = A.A_Var5_FK_LK
    INNER JOIN TableD (NOLOCK) D ON D.D_Var1_PK     = A.A_Var6_FK_LK
    INNER JOIN TableE (NOLOCK) E ON E.E_Var1_PK     = A.A_Var7_FK_LK  
    LEFT OUTER JOIN feeds.TableF (NOLOCK) F ON F.F_Var1 = T.TTVar5
WHERE A.A_Var8_FK_LK = @Param1
GROUP BY
    CASE E.E_Var1_LK
        WHEN 'Text1' THEN T.TTVar2_LK + '_' + F.F_Var1
        WHEN 'Text2' THEN T.TTVar2_LK + '_' + F.F_Var2
        WHEN 'Text3' THEN T.TTVar2_LK
    END,
    T.TTVar4_LK,
    T.TTVar3_LK,
    CASE E.E_Var1_LK 
        WHEN 'Text1' THEN F.F_Var1
        WHEN 'Text2' THEN F.F_Var2
        WHEN 'Text3' THEN T.TTVar5
    END,
    A.A_Var3_FK_LK, 
    C.C_Var1_PK


IF OBJECT_ID(N'tempdb..#TempTable') IS NOT NULL
BEGIN
    DROP TABLE #TempTable
END
IF OBJECT_ID(N'tempdb..#TempTable') IS NOT NULL
BEGIN
    DROP TABLE #TempTable
END

Lo que he encontrado es que la tercera declaración (comentada como lenta) es la parte que lleva más tiempo. Las dos declaraciones anteriores vuelven casi al instante.

El plan de ejecución está disponible como XML en este enlace .

Es mejor hacer clic derecho y guardar y luego abrir en SQL Sentry Plan Explorer o algún otro software de visualización en lugar de abrirlo en su navegador.

Si necesita más información sobre las tablas o los datos, no dude en preguntar.

Neo
fuente
2
Tus estadísticas están muy lejos. ¿Cuándo es la última vez que des-fragmentaste índices o estadísticas actualizadas? Además, trataría de usar una tabla temporal, en lugar de la variable de tabla, @MyTableVar, ya que el optimizador realmente no puede usar estadísticas en las variables de la tabla.
Adam Haines
Gracias por tu respuesta Adam. Cambiar el @MyTableVar a una tabla temporal no tiene ningún efecto, pero es solo un pequeño número de filas (que se puede ver desde el plan de ejecución). ¿Qué muestra el plan de ejecución que mis estadísticas están muy lejos? ¿Indica qué índices deben reorganizarse o reconstruirse, y qué tabla (s) deben tener estadísticas actualizadas?
Neo
3
Esa unión hash en la parte inferior derecha tiene un estimado de 24,000 filas en la entrada de compilación, pero 3,285,620 reales, por lo que bien podría estar derramándose tempdb. es decir, las estimaciones para las filas resultantes de la unión entre TableAy @MyTableVarestán muy alejadas. Además, el número de filas que van a los tipos es mucho mayor de lo estimado, por lo que también podrían estar derramándose.
Martin Smith

Respuestas:

22

Antes de llegar a la respuesta principal, hay dos piezas de software que necesita actualizar.

Actualizaciones de software requeridas

El primero es SQL Server. Está ejecutando SQL Server 2008 Service Pack 1 (compilación 2531). Debería estar actualizado al menos al Service Pack actual (SQL Server 2008 Service Pack 3 - build 5500). La versión más reciente de SQL Server 2008 en el momento de la escritura es Service Pack 3, Actualización acumulativa 12 (versión 5844).

La segunda pieza de software es SQL Sentry Plan Explorer . Las últimas versiones tienen nuevas características y correcciones significativas, incluida la capacidad de cargar directamente un plan de consulta para análisis experto (¡no es necesario pegar XML en ningún lado!)

Análisis del plan de consulta

La estimación de cardinalidad para la variable de tabla es exactamente correcta, gracias a una recopilación a nivel de declaración:

tabla de estimación variable

Desafortunadamente, las variables de tabla no mantienen estadísticas de distribución, por lo que todo lo que sabe el optimizador es que hay seis filas; no sabe nada de los valores que podrían estar en esas seis filas. Esta información es crucial dado que la siguiente operación es unir a otra tabla. La estimación de cardinalidad de esa unión se basa en una suposición descabellada del optimizador:

primer presupuesto estimado

A partir de ese momento, el plan elegido por el optimizador se basa en información incorrecta, por lo que no es de extrañar que el rendimiento sea tan bajo. En particular, la memoria reservada para géneros y tablas hash para combinaciones hash será demasiado pequeña. En el momento de la ejecución, los desbordamientos y las operaciones de hash se derramarán en el disco físico tempdb.

SQL Server 2008 no destaca esto en los planes de ejecución; puede monitorear los derrames usando Extended Events o Profiler advertencias de clasificación de perfil y advertencias de hash . La memoria está reservada para tipos y hashes basados ​​en estimaciones de cardinalidad antes de que comience la ejecución, y no se puede aumentar durante la ejecución independientemente de la cantidad de memoria libre que pueda tener su SQL Server. Por lo tanto, las estimaciones precisas del recuento de filas son cruciales para cualquier plan de ejecución que implique operaciones que consuman memoria en el espacio de trabajo.

Su consulta también está parametrizada. Deberías considerar agregarOPTION (RECOMPILE) a la consulta si diferentes valores de parámetros afectan el plan de consulta. Probablemente debería considerar usarlo de todos modos, para que el optimizador pueda ver el valor @Param1en el momento de la compilación. Por lo menos, esto puede ayudar al optimizador a producir una estimación más razonable para la búsqueda de índice que se muestra arriba, dado que la tabla es muy grande y está particionada. También puede habilitar la eliminación de particiones estáticas.

Intente la consulta nuevamente con una tabla temporal en lugar de la variable de tabla y OPTION (RECOMPILE) . También debe intentar materializar el resultado de la primera unión en otra tabla temporal y ejecutar el resto de la consulta en ese sentido. El número de filas no es tan grande (3,285,620), por lo que esto debería ser razonablemente rápido. El optimizador tendrá una estimación de cardinalidad exacta y estadísticas de distribución para el resultado de la unión. Con suerte, el resto del plan encajará bien.

Trabajando a partir de las propiedades que se muestran en el plan, la consulta de materialización sería:

SELECT
    A.A_Var7_FK_LK,
    A.A_Var4_FK_LK,
    A.A_Var6_FK_LK, 
    A.A_Var5_FK_LK,
    A.A_Var1,
    A.A_Var2,
    A.A_Var3_FK_LK
INTO #AnotherTempTable
FROM @MyTableVar AS B
JOIN TableA AS A
    ON A.Job = B.B_Var1_PK
WHERE
    A_Var8_FK_LK = @Param1;

También podría INSERTingresar a una tabla temporal predefinida (los tipos de datos correctos no se muestran en el plan, por lo que no puedo hacer esa parte). La nueva tabla temporal puede o no beneficiarse de los índices agrupados y no agrupados.

Paul White reinstala a Monica
fuente
Muchas gracias por esta respuesta en profundidad. Lo siento, me tomó una semana responder: he estado trabajando en esto todos los días intercalado con otro trabajo. He implementado sus sugerencias materializando la unión a TableA #AnotherTempTable. Esto pareció tener el mejor impacto: las otras sugerencias (el uso de una tabla temporal en lugar de una variable de tabla para @MyTableVar y el uso OPTION (RECOMPILE)no tuvieron mucho efecto o ninguno. El 'Anonimizar' y 'Publicar en SQLPerformance.com' las opciones en SQL Sentry Plan Explorer son geniales: las acabo de usar: answers.sqlperformance.com/questions/1087
Neo
-6

Noté que debería haber un PK en @MyTableVar y estoy de acuerdo en que #MyTableVar a menudo tiene un mejor rendimiento (especialmente con un mayor número de filas).

La condición dentro de la cláusula where

   WHERE A.A_Var8_FK_LK = @Param1

debe moverse a la unión interna A AND'ed. El optimizador no es lo suficientemente inteligente en mi experiencia para hacer esto (lo siento, no miró el plan) y puede hacer una gran diferencia.

Si esos cambios no muestran una mejora, luego crearía otra tabla temporal de A y todas las cosas a las que se une restringido (¿bien?) Por A.A_Var8_FK_LK = @ Param1 si esa agrupación tiene sentido lógico para usted.

Luego, cree un índice agrupado en esa tabla temporal (antes o después de la creación) para la siguiente condición de unión.

Luego, une ese resultado a las pocas tablas (F y T) que quedan.

Bam, que necesita un plan de consulta apestoso cuando las estimaciones de la fila están apagadas y, a veces, no son fáciles de mejorar ). Sin embargo, supongo que tiene índices adecuados, que es lo que primero verificaría dentro del plan.

Una traza puede mostrar los derrames tempdb que pueden tener o no un impacto drástico.

Otro enfoque alternativo, que es más rápido probar al menos, es ordenar las tablas desde el número más bajo de filas (A) hasta el más alto y luego comenzar a agregar la fusión, el hash y el bucle a las uniones. Cuando hay sugerencias, el orden de unión se fija como se especifica. Otros usuarios evitan sabiamente este enfoque porque a largo plazo puede doler si el recuento relativo de filas cambia drásticamente. Es deseable un número mínimo de pistas.

Si está haciendo muchos de estos, quizás valga la pena probar (o probar) un optimizador comercial y siga siendo una buena experiencia de aprendizaje.

crokusek
fuente
Sí lo es. Asegura que las filas devueltas por A están limitadas por la restricción. De lo contrario, el optimizador puede unirse primero y aplicar la restricción después. Yo trato con esto a diario.
crokusek
44
@crokusek Estás equivocado. El optimizador de SQL-Server es bastante bueno para saber que las consultas son equivalentes (si una condición está en WHERE o en la cláusula ON) cuando se trata de una unión INNER.
ypercubeᵀᴹ
66
Puede encontrar útil la serie de Paul White en el Optimizador de consultas .
Martin Smith
Es un hábito terrible. Tal vez lo sea para este caso particular (donde hay una restricción), pero vengo de la tierra de múltiples desarrolladores que acumulan condiciones AND en la cláusula where. SQL Server hace que no siempre "movimiento" de vuelta a la unión para usted.
crokusek
Acepto incorrecto para uniones externas (y correctas). Pero cuando solo hay expresiones AND 'dentro de una cláusula where y cada término corresponde únicamente a una unión interna específica, ese término se puede mover de manera segura y confiable a la ubicación "activada" como una optimización y una mejor práctica (imo). Si es una condición de unión "verdadera" o simplemente una restricción fija es secundaria a una gran ganancia de rendimiento. Ese enlace es para un caso trivial. La vida real tiene múltiples condiciones de convert () y matemáticas, lo que los convierte en mejores candidatos para derivar las mejores prácticas.
crokusek