Ho una domanda sulle prestazioni delle espressioni di tabella comuni in SQL Server. Nel nostro team di sviluppatori utilizziamo un sacco di concatenazione di CTE durante la creazione delle nostre query. Attualmente sto lavorando su una query che ha avuto prestazioni terribili. Ma ho scoperto che se nel mezzo della catena ho inserito tutti i record fino a quel CTE in una tabella temporanea invece e poi ho continuato, ma selezionando da quella tabella temporanea ho migliorato significativamente le prestazioni. Ora vorrei avere un aiuto per capire se questo tipo di modifica si applica solo a questa query specifica e perché i due casi che vedrete qui di seguito differiscono così tanto nelle prestazioni. Oppure potremmo forse abusare delle CTE nel nostro team e possiamo ottenere prestazioni generalmente imparando da questo caso?T-SQL con prestazioni scadenti con CTE
prega di cercare di spiegare a me esattamente cosa sta succedendo qui ...
Il codice è completo e si sarà in grado di eseguirlo su SQL Server 2008 e probabilmente anche 2005. Una parte è commentata e la mia idea è che è possibile cambiare i due casi commentando l'uno o l'altro. Puoi vedere dove inserire i commenti dei blocchi, ho contrassegnato questi posti con --block comment here
e --end block comment here
È il caso a esecuzione lenta che è un valore predefinito non commentato. Ecco a voi:
--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;
Se volete per cercare di capire quello che sto facendo nell'ultimo passaggio ho una domanda SO su di esso here.
Per il caso 1 impiega circa 34 secondi e il caso 2 impiega circa 2 secondi. La differenza è che io memorizzo il risultato di S in una tabella temporanea nel caso 2, nel caso 1 io uso S nel mio prossimo CTE direttamente.
+1 per codice eseguibile. Suggerisco di eseguirli entrambi e incollare il piano di esecuzione XML in SQL Sentry Plan Explorer. La ragione della differenza sarà quindi abbastanza evidente. Finisce la scansione di "Preparazione #" 20.000 volte in una parte del piano e 10.000 volte in un'altra parte per un esempio. –
Grazie. Ho installato SQL Sentry Plan Explorer. Sto ancora imparando SQL Server e non riesco a leggere i piani di esecuzione, né in Sentry Plan. Ma la risposta è che non si può dire nulla del suo meglio con CTE o tabella temporanea in determinati scenari senza indagare sui piani di esecuzione? Dove vedi 20.000 e 10.000 volte? Non riesco a trovarlo È possibile dire qualcosa sul perché una parte sta scansionando #Preparazione 20.000 volte facendo riferimento al codice? – John
Hai visto questo? "Prestazioni di tabelle CTE e TEMP di SQL 2005 quando utilizzate nei join di altre tabelle" http://stackoverflow.com/questions/1531835/sql-2005-cte-vs-temp-table-performance-when-used-in-joins- of-other-tables –