2012-05-25 14 views
16

Tengo una pregunta de rendimiento sobre las expresiones de tabla común en SQL Server. En nuestro equipo de desarrolladores, utilizamos una gran cantidad de CTE de encadenamiento al crear nuestras consultas. Actualmente estoy trabajando en una consulta que tuvo un rendimiento terrible. Pero descubrí que si en el medio de la cadena insertaba todos los registros hasta ese CTE en una tabla temporal y luego continuaba, pero seleccionando desde esa tabla temporal, mejoré significativamente el rendimiento. Ahora me gustaría obtener ayuda para entender si este tipo de cambio solo se aplica a esta consulta específica y por qué los dos casos que verá a continuación difieren tanto en el rendimiento. ¿O podríamos abusar de CTE en nuestro equipo y podemos obtener rendimiento en general al aprender de este caso?Mal rendimiento de T-SQL con CTE

Por favor, intente explicar a mí exactamente lo que está sucediendo aquí ...

El código está completo y que será capaz de ejecutarlo en SQL Server 2008 y 2005, probablemente, también. Una parte está comentada y mi idea es que puedes cambiar los dos casos por un comentario de uno u otro. Puede ver dónde colocar sus comentarios de bloque, he marcado estos lugares con --block comment here y --end block comment here

Es el caso de ejecución lenta que no está descompuesto por defecto. Aquí está:

--Declare tables to use in example. 
CREATE TABLE #Preparation 
(
    Date DATETIME NOT NULL 
    ,Hour INT NOT NULL 
    ,Sales NUMERIC(9,2) 
    ,Items INT 
); 

CREATE TABLE #Calendar 
(
    Date DATETIME NOT NULL 
) 

CREATE TABLE #OpenHours 
(
    Day INT NOT NULL, 
    OpenFrom TIME NOT NULL, 
    OpenTo TIME NOT NULL 
); 

--Fill tables with sample data. 
INSERT INTO #OpenHours (Day, OpenFrom, OpenTo) 
VALUES 
    (1, '10:00', '20:00'), 
    (2, '10:00', '20:00'), 
    (3, '10:00', '20:00'), 
    (4, '10:00', '20:00'), 
    (5, '10:00', '20:00'), 
    (6, '10:00', '20:00'), 
    (7, '10:00', '20:00') 

DECLARE @CounterDay INT = 0, @CounterHour INT = 0, @Sales NUMERIC(9, 2), @Items INT; 

WHILE @CounterDay < 365 
BEGIN 
    SET @CounterHour = 0; 
    WHILE @CounterHour < 5 
    BEGIN 
     SET @Items = CAST(RAND() * 100 AS INT); 
     SET @Sales = CAST(RAND() * 1000 AS NUMERIC(9, 2)); 
     IF @Items % 2 = 0 
     BEGIN 
      SET @Items = NULL; 
      SET @Sales = NULL; 
     END 

     INSERT INTO #Preparation (Date, Hour, Items, Sales) 
     VALUES (DATEADD(DAY, @CounterDay, '2011-01-01'), @CounterHour + 13, @Items, @Sales); 

     SET @CounterHour += 1; 
    END 
    INSERT INTO #Calendar (Date) VALUES (DATEADD(DAY, @CounterDay, '2011-01-01')); 
    SET @CounterDay += 1; 
END 

--Here the query starts. 
;WITH P AS (
    SELECT DATEADD(HOUR, Hour, Date) AS Hour 
     ,Sales 
     ,Items 
    FROM #Preparation 
), 
O AS (
     SELECT DISTINCT DATEADD(HOUR, SV.number, C.Date) AS Hour 
     FROM #OpenHours AS O 
      JOIN #Calendar AS C ON O.Day = DATEPART(WEEKDAY, C.Date) 
      JOIN master.dbo.spt_values AS SV ON SV.number BETWEEN DATEPART(HOUR, O.OpenFrom) AND DATEPART(HOUR, O.OpenTo) 
), 
S AS (
    SELECT O.Hour, P.Sales, P.Items 
    FROM O 
     LEFT JOIN P ON P.Hour = O.Hour 
) 

--block comment here case 1 (slow performing) 
--With this technique it takes about 34 seconds. 
,N AS (
     SELECT 
      A.Hour 
      ,A.Sales AS SalesOrg 
      ,CASE WHEN COALESCE(B.Sales, C.Sales, 1) < 0 
       THEN 0 ELSE COALESCE(B.Sales, C.Sales, 1) END AS Sales 
      ,A.Items AS ItemsOrg 
      ,COALESCE(B.Items, C.Items, 1) AS Items 
     FROM S AS A 
     OUTER APPLY (SELECT TOP 1 * 
        FROM S 
        WHERE Hour <= A.Hour 
         AND Sales IS NOT NULL 
         AND DATEDIFF(DAY, Hour, A.Hour) = 0      
        ORDER BY Hour DESC) B 
     OUTER APPLY (SELECT TOP 1 * 
        FROM S 
        WHERE Sales IS NOT NULL 
         AND DATEDIFF(DAY, Hour, A.Hour) = 0 
        ORDER BY Hour) C 
    ) 
--end block comment here case 1 (slow performing) 

/*--block comment here case 2 (fast performing) 
--With this technique it takes about 2 seconds. 
SELECT * INTO #tmpS FROM S; 

WITH 
N AS (
     SELECT 
      A.Hour 
      ,A.Sales AS SalesOrg 
      ,CASE WHEN COALESCE(B.Sales, C.Sales, 1) < 0 
       THEN 0 ELSE COALESCE(B.Sales, C.Sales, 1) END AS Sales 
      ,A.Items AS ItemsOrg 
      ,COALESCE(B.Items, C.Items, 1) AS Items 
     FROM #tmpS AS A 
     OUTER APPLY (SELECT TOP 1 * 
        FROM #tmpS 
        WHERE Hour <= A.Hour 
         AND Sales IS NOT NULL 
         AND DATEDIFF(DAY, Hour, A.Hour) = 0      
        ORDER BY Hour DESC) B 
     OUTER APPLY (SELECT TOP 1 * 
        FROM #tmpS 
        WHERE Sales IS NOT NULL 
         AND DATEDIFF(DAY, Hour, A.Hour) = 0 
        ORDER BY Hour) C 
    ) 
--end block comment here case 2 (fast performing)*/ 
SELECT * FROM N ORDER BY Hour 


IF OBJECT_ID('tempdb..#tmpS') IS NOT NULL DROP TABLE #tmpS; 

DROP TABLE #Preparation; 
DROP TABLE #Calendar; 
DROP TABLE #OpenHours; 

Si desea para tratar de entender lo que estoy haciendo en el último paso Tengo una pregunta al respecto SO here.

Para mí, el caso 1 tarda unos 34 segundos y el caso 2 tarda unos 2 segundos. La diferencia es que almaceno el resultado de S en una tabla temporal en el caso 2, en el caso 1, utilizo S en mi próximo CTE directamente.

+1

+1 Para código ejecutable. Sugiero ejecutarlos ambos y pegar el plan de ejecución XML en SQL Sentry Plan Explorer. La razón de la diferencia será entonces bastante evidente. Termina escaneando '# Preparation '20,000 veces en una parte del plan y 10,000 veces en otra parte para un ejemplo. –

+0

Gracias. Instalé SQL Sentry Plan Explorer. Todavía estoy aprendiendo SQL Server y no puedo leer planes de ejecución, ni en Sentry Plan. Pero la respuesta es que uno no puede decir nada acerca de si es mejor con CTE o tabla temporal en ciertos escenarios sin investigar los planes de ejecución. ¿Dónde ves 20,000 y 10,000 veces? No puedo encontrarlo ¿Es posible decir algo sobre por qué una parte está escaneando #preparación 20,000 veces al referirse al código? – John

+0

¿Has visto esto? "SQL 2005 CTE vs TEMP tabla Rendimiento cuando se utiliza en las uniones de otras tablas" http://stackoverflow.com/questions/1531835/sql-2005-cte-vs-temp-table-performance-when-used-in-joins- of-other-tables –

Respuesta

5

CTE es simplemente un atajo de sintaxis. Ese CTE se ejecuta (y vuelve a ejecutar) en el join. Con #temp se evalúa una vez y luego los resultados se vuelven a usar en la unión.

La documentación es engañosa.

MSDN_CTE

Una expresión de tabla común (CTE) puede ser pensado como un conjunto de resultados temporal.

En este artículo se explica mejor

PapaCTEarticle

Un CTE es un buen ajuste para este tipo de escenario ya que hace que el T-SQL mucho más legible (como una vista), sin embargo, se puede utilizar más de una vez en una consulta que sigue inmediatamente en el mismo lote. Por supuesto, no está disponible más allá de ese alcance. Además, el CTE es una construcción a nivel de lenguaje, lo que significa que SQL Server no crea internamente tablas temporales o virtuales. Se llamará a la consulta subyacente del CTE cada vez que se haga referencia a ella en la consulta inmediatamente siguiente.

Echa un vistazo a su valor Parámetros de la tabla

TVP

Tienen la estructura como un #temp pero no tanta sobrecarga. Son de solo lectura, pero parece que solo necesita leer solo. La creación y eliminación de un #temp variará, pero en un servidor de nivel bajo a medio es un hit de 0.1 segundos y con TVP no hay ningún hit.

+0

Los TVP no son una buena recomendación para esto. Existe un potencial de muchos problemas con ellos, entre los que destaca que existen fuera del alcance transaccional y, por lo tanto, no se pueden deshacer. – JNK

+0

@JNK Ayúdame. TVP solo es de lectura, ¿por qué me importa si no se puede revertir? Esta muestra es solo seleccionar sin insertar, actualizar o eliminar. ¿Dónde está el riesgo? Me has ayudado más de una vez y siempre has estado en lo correcto. – Paparazzi

+1

Tengo una desconfianza general de los TVP por cualquier cosa que no sea salida o prueba. Aún escriben en el archivo de registro y tienen sobrecarga de E/S de disco, pero en realidad no son atómicos y no pueden indexarse, no tienen estadísticas, y los planes de ejecución siempre asumen una sola fila para ellos. – JNK

11

A CTE es esencialmente solo una vista desechable.Prácticamente nunca hará una consulta más rápida que simplemente poner el código CTE en una cláusula FROM como una expresión de tabla.

En su ejemplo, el verdadero problema son las funciones de fecha, creo.

Su primer caso (lento) requiere que las funciones de fecha se ejecuten para cada fila.

Para su segundo caso (más rápido) se ejecutan una vez y se almacenan en una tabla.

Esto normalmente no es tan notable a menos que haga algún tipo de lógica en el campo derivado de la función. En su caso, usted está haciendo un ORDER BY en Hour, que es muy costoso. En el segundo ejemplo, se trata de un ordenamiento simple en un campo, pero en el primero se está ejecutando esa función para cada fila, ENTONCES ordenando.

Para una lectura mucho más profunda en CTE, consulte this question on DBA.SE.

Cuestiones relacionadas