2012-05-25 18 views
16

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

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

+0

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

+0

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 –

risposta

5

CTE è solo una scorciatoia di sintassi. Quella CTE viene eseguita (e rieseguita) nel join. Con #temp viene valutato una volta e quindi i risultati vengono riutilizzati nel join.

La documentazione è fuorviante.

MSDN_CTE

Un'espressione di tabella comune (CTE) può essere pensato come insieme risultato temporaneo.

Questo articolo spiega meglio

PapaCTEarticle

Un CTE è una forma piacevole per questo tipo di scenario, poiché rende il T-SQL molto più leggibile (come una vista), ma può essere utilizzato più di una volta in una query che segue immediatamente nello stesso batch. Certo, non è disponibile oltre questo scopo. Inoltre, il CTE è un costrutto a livello di linguaggio, il che significa che SQL Server non crea internamente tabelle temporanee o virtuali. La query sottostante della CTE verrà chiamata ogni volta che viene fatto riferimento nella query immediatamente successiva.

Date un'occhiata a tabella dei valori dei parametri

TVP

Hanno la struttura come un #temp ma non tanto in alto. Sono di sola lettura ma sembra che tu abbia solo bisogno di sola lettura. La creazione e il rilascio di un #temp varieranno, ma su un server medio-basso è un successo di 0,1 secondi e con TVP non c'è essenzialmente hit.

+0

TVPs non è una buona raccomandazione per questo. Esiste potenzialmente un sacco di problemi con loro, non ultimo dei quali è che esistono al di fuori dello scope transazionale e quindi non possono essere ripristinati! – JNK

+0

@ JNK Aiutami. TVP è di sola lettura, quindi perché mi interessa se non può essere ripristinato. Questo esempio è selezionato solo senza inserire, aggiornare o eliminare. Dov'è il rischio? Mi hai aiutato più di una volta e hai sempre avuto ragione. – Paparazzi

+1

Ho una sfiducia generale nei TVP per qualcosa di diverso dall'output o dal testing. Scrivono ancora nel file di log e hanno un sovraccarico dell'IO su disco, ma non sono in realtà atomici e non possono essere indicizzati, non dispongono di statistiche e i piani di esecuzione presuppongono sempre una singola riga per essi. – JNK

11

Un CTE è essenzialmente una vista monouso.Praticamente non effettuerà mai una query più velocemente del semplice inserimento del codice CTE in una clausola FROM come espressione di tabella.

Nel tuo esempio, il vero problema sono le funzioni di data in cui credo.

Il primo caso (lento) richiede l'esecuzione delle funzioni di data per ogni riga.

Per il secondo caso (più veloce) vengono eseguiti una volta e memorizzati in un tavolo.

Questo non è normalmente così evidente se non si fa una sorta di logica sul campo derivato dalla funzione. Nel tuo caso stai facendo uno ORDER BY su Hour, che è molto costoso. Nel tuo secondo esempio è un ordinamento semplice su un campo, ma nel primo si esegue quella funzione per ogni riga, POI l'ordinamento.

Per una lettura molto più approfondita su CTE, vedere this question on DBA.SE.

Problemi correlati