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 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. –
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
¿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 –