Questo articolo è la settima parte di una serie sulle espressioni di tabelle con nome. Nella parte 5 e nella parte 6 ho trattato gli aspetti concettuali delle espressioni di tabelle comuni (CTE). Questo mese e il prossimo il mio focus si sposta sulle considerazioni sull'ottimizzazione dei CTE.
Inizierò rivisitando rapidamente il concetto di annullamento dell'annidamento delle espressioni di tabelle con nome e dimostrerò la sua applicabilità ai CTE. Poi mi concentrerò su considerazioni di persistenza. Parlerò degli aspetti di persistenza dei CTE ricorsivi e non ricorsivi. Spiegherò quando ha senso attenersi ai CTE rispetto a quando ha effettivamente più senso lavorare con tabelle temporanee.
Nei miei esempi continuerò a utilizzare i database di esempio TSQLV5 e PerformanceV5. Puoi trovare lo script che crea e popola TSQLV5 qui e il suo diagramma ER qui. Puoi trovare lo script che crea e popola PerformanceV5 qui.
Sostituzione/annullamento dell'annidamento
Nella parte 4 della serie, incentrata sull'ottimizzazione delle tabelle derivate, ho descritto un processo di annullamento/sostituzione delle espressioni di tabella. Ho spiegato che quando SQL Server ottimizza una query che coinvolge tabelle derivate, applica regole di trasformazione all'albero iniziale degli operatori logici prodotti dal parser, eventualmente spostando le cose attraverso quelli che originariamente erano i limiti dell'espressione della tabella. Ciò accade al punto che quando si confronta un piano per una query che utilizza tabelle derivate con un piano per una query che va direttamente rispetto alle tabelle di base sottostanti in cui è stata applicata la logica di disannidamento, hanno lo stesso aspetto. Ho anche descritto una tecnica per impedire l'annullamento dell'annidamento utilizzando il filtro TOP con un numero molto elevato di righe come input. Ho illustrato un paio di casi in cui questa tecnica è stata molto utile, uno in cui l'obiettivo era evitare errori e un altro per motivi di ottimizzazione.
La versione TL;DR di sostituzione/annullamento dell'annidamento di CTE prevede che il processo sia lo stesso delle tabelle derivate. Se sei soddisfatto di questa affermazione, puoi sentirti libero di saltare questa sezione e passare direttamente alla sezione successiva sulla Persistenza. Non ti perderai nulla di importante che non hai letto prima. Tuttavia, se sei come me, probabilmente vorrai la prova che è davvero così. Quindi, probabilmente vorrai continuare a leggere questa sezione e testare il codice che utilizzo mentre rivisito esempi di disnidimento chiave che ho dimostrato in precedenza con tabelle derivate e convertirli per utilizzare CTE.
Nella parte 4 ho dimostrato la seguente query (la chiameremo Query 1):
USE TSQLV5; SELECT orderid, orderdate FROM ( SELECT * FROM ( SELECT * FROM ( SELECT * FROM Sales.Orders WHERE orderdate >= '20180101' ) AS D1 WHERE orderdate >= '20180201' ) AS D2 WHERE orderdate >= '20180301' ) AS D3 WHERE orderdate >= '20180401';
La query prevede tre livelli di nidificazione di tabelle derivate, oltre a una query esterna. Ciascun livello filtra un diverso intervallo di date degli ordini. Il piano per la query 1 è mostrato nella figura 1.
Figura 1:piano di esecuzione per la query 1
Il piano nella Figura 1 mostra chiaramente che la rimozione dell'annidamento delle tabelle derivate ha avuto luogo poiché tutti i predicati di filtro sono stati uniti in un unico predicato di filtro globale.
Ho spiegato che puoi impedire il processo di annullamento dell'annidamento utilizzando un filtro TOP significativo (al contrario di TOP 100 PERCENT) con un numero molto elevato di righe come input, come mostra la seguente query (la chiameremo Query 2):
SELECT orderid, orderdate FROM ( SELECT TOP (9223372036854775807) * FROM ( SELECT TOP (9223372036854775807) * FROM ( SELECT TOP (9223372036854775807) * FROM Sales.Orders WHERE orderdate >= '20180101' ) AS D1 WHERE orderdate >= '20180201' ) AS D2 WHERE orderdate >= '20180301' ) AS D3 WHERE orderdate >= '20180401';
Il piano per la query 2 è mostrato nella Figura 2.
Figura 2:piano di esecuzione per la query 2
Il piano mostra chiaramente che lo snidamento non è avvenuto poiché puoi vedere in modo efficace i limiti della tabella derivata.
Proviamo gli stessi esempi usando i CTE. Ecco la query 1 convertita per utilizzare CTE:
WITH C1 AS ( SELECT * FROM Sales.Orders WHERE orderdate >= '20180101' ), C2 AS ( SELECT * FROM C1 WHERE orderdate >= '20180201' ), C3 AS ( SELECT * FROM C2 WHERE orderdate >= '20180301' ) SELECT orderid, orderdate FROM C3 WHERE orderdate >= '20180401';
Ottieni lo stesso identico piano mostrato in precedenza nella Figura 1, dove puoi vedere che è avvenuto lo snidamento.
Ecco la query 2 convertita per utilizzare CTE:
WITH C1 AS ( SELECT TOP (9223372036854775807) * FROM Sales.Orders WHERE orderdate >= '20180101' ), C2 AS ( SELECT TOP (9223372036854775807) * FROM C1 WHERE orderdate >= '20180201' ), C3 AS ( SELECT TOP (9223372036854775807) * FROM C2 WHERE orderdate >= '20180301' ) SELECT orderid, orderdate FROM C3 WHERE orderdate >= '20180401';
Ottieni lo stesso piano mostrato in precedenza nella Figura 2, dove puoi vedere che lo snidamento non è avvenuto.
Quindi, rivisitiamo i due esempi che ho usato per dimostrare la praticità della tecnica per prevenire il disannidamento, solo che questa volta utilizzando CTE.
Iniziamo con la query errata. La query seguente tenta di restituire righe ordine con uno sconto maggiore dello sconto minimo e in cui il reciproco dello sconto è maggiore di 10:
SELECT orderid, productid, discount FROM Sales.OrderDetails WHERE discount > (SELECT MIN(discount) FROM Sales.OrderDetails) AND 1.0 / discount > 10.0;
Lo sconto minimo non può essere negativo, ma nullo o superiore. Quindi, probabilmente stai pensando che se una riga ha uno sconto zero, il primo predicato dovrebbe restituire false e che un cortocircuito dovrebbe impedire il tentativo di valutare il secondo predicato, evitando così un errore. Tuttavia, quando esegui questo codice, ottieni un errore di divisione per zero:
Msg 8134, Level 16, State 1, Line 99 Divide by zero error encountered.
Il problema è che anche se SQL Server supporta un concetto di cortocircuito a livello di elaborazione fisica, non vi è alcuna garanzia che valuterà i predicati del filtro in ordine scritto da sinistra a destra. Un tentativo comune di evitare tali errori consiste nell'usare un'espressione di tabella denominata che gestisce la parte della logica di filtro che si desidera valutare per prima e fare in modo che la query esterna gestisca la logica di filtro che si desidera valutare per seconda. Ecco la soluzione tentata utilizzando un CTE:
WITH C AS ( SELECT * FROM Sales.OrderDetails WHERE discount > (SELECT MIN(discount) FROM Sales.OrderDetails) ) SELECT orderid, productid, discount FROM C WHERE 1.0 / discount > 10.0;
Sfortunatamente, tuttavia, l'annullamento dell'annidamento dell'espressione della tabella risulta in un equivalente logico della query della soluzione originale e quando si tenta di eseguire questo codice si ottiene nuovamente un errore di divisione per zero:
Msg 8134, Level 16, State 1, Line 108 Divide by zero error encountered.
Usando il nostro trucco con il filtro TOP nella query interna, impedisci l'annullamento dell'annidamento dell'espressione della tabella, in questo modo:
WITH C AS ( SELECT TOP (9223372036854775807) * FROM Sales.OrderDetails WHERE discount > (SELECT MIN(discount) FROM Sales.OrderDetails) ) SELECT orderid, productid, discount FROM C WHERE 1.0 / discount > 10.0;
Questa volta il codice viene eseguito correttamente senza errori.
Procediamo con l'esempio in cui si utilizza la tecnica per impedire il disannidamento per motivi di ottimizzazione. Il codice seguente restituisce solo i mittenti con una data massima dell'ordine pari o successiva al 1 gennaio 2018:
USE PerformanceV5; WITH C AS ( SELECT S.shipperid, (SELECT MAX(O.orderdate) FROM dbo.Orders AS O WHERE O.shipperid = S.shipperid) AS maxod FROM dbo.Shippers AS S ) SELECT shipperid, maxod FROM C WHERE maxod >= '20180101';
Se ti stai chiedendo perché non utilizzare una soluzione molto più semplice con una query raggruppata e un filtro HAVING, ha a che fare con la densità della colonna shipperid. La tabella Ordini contiene 1.000.000 di ordini e le spedizioni di tali ordini sono state gestite da cinque spedizionieri, il che significa che in media ogni spedizioniere ha gestito il 20% degli ordini. Il piano per una query raggruppata che calcola la data massima dell'ordine per mittente scansionerebbe tutte le 1.000.000 di righe, risultando in migliaia di letture di pagine. Infatti, se evidenzi solo la query interna del CTE (la chiameremo Query 3) calcolando la data massima dell'ordine per mittente e ne controlli il piano di esecuzione, otterrai il piano mostrato nella Figura 3.
Figura 3:Piano di esecuzione per la query 3
Il piano esegue la scansione di cinque righe nell'indice cluster su Utenti. Per mittente, il piano applica una ricerca rispetto a un indice di copertura sugli ordini, dove (shipperid, orderdate) sono le chiavi iniziali dell'indice, andando direttamente all'ultima riga in ciascuna sezione del mittente a livello di foglia per estrarre la data massima dell'ordine per l'attuale spedizioniere. Dal momento che abbiamo solo cinque caricatori, ci sono solo cinque operazioni di ricerca di indici, risultando in un piano molto efficiente. Ecco le misure delle prestazioni che ho ottenuto quando ho eseguito la query interna del CTE:
duration: 0 ms, CPU: 0 ms, reads: 15
Tuttavia, quando esegui la soluzione completa (la chiameremo Query 4), ottieni un piano completamente diverso, come mostrato nella Figura 4.
Figura 4:Piano di esecuzione per la query 4
Quello che è successo è che SQL Server ha annullato l'annidamento dell'espressione della tabella, convertendo la soluzione in un equivalente logico di una query raggruppata, risultando in un'analisi completa dell'indice su Orders. Ecco i numeri delle prestazioni che ho ottenuto per questa soluzione:
duration: 316 ms, CPU: 281 ms, reads: 3854
Ciò di cui abbiamo bisogno qui è impedire che avvenga l'annullamento dell'annidamento dell'espressione della tabella, in modo che la query interna venga ottimizzata con le ricerche rispetto all'indice su Orders e che la query esterna si traduca semplicemente nell'aggiunta di un operatore Filter nel Piano. Puoi ottenere questo risultato usando il nostro trucco aggiungendo un filtro TOP alla query interna, in questo modo (chiameremo questa soluzione Query 5):
WITH C AS ( SELECT TOP (9223372036854775807) S.shipperid, (SELECT MAX(O.orderdate) FROM dbo.Orders AS O WHERE O.shipperid = S.shipperid) AS maxod FROM dbo.Shippers AS S ) SELECT shipperid, maxod FROM C WHERE maxod >= '20180101';
Il piano per questa soluzione è mostrato nella Figura 5.
Figura 5:Piano di esecuzione per la query 5
Il piano mostra che l'effetto desiderato è stato raggiunto e di conseguenza i numeri di performance lo confermano:
duration: 0 ms, CPU: 0 ms, reads: 15
Pertanto, i nostri test confermano che SQL Server gestisce la sostituzione/annullamento dell'annidamento di CTE proprio come fa per le tabelle derivate. Ciò significa che non dovresti preferire l'uno all'altro per motivi di ottimizzazione, ma per differenze concettuali che ti interessano, come discusso nella Parte 5.
Persistenza
Un malinteso comune riguardo ai CTE e alle espressioni di tabelle con nome in generale è che servano come una sorta di veicolo di persistenza. Alcuni pensano che SQL Server persista il set di risultati della query interna in una tabella di lavoro e che la query esterna interagisca effettivamente con tale tabella di lavoro. In pratica, le normali CTE non ricorsive e le tabelle derivate non vengono mantenute. Ho descritto la logica di annullamento dell'annidamento applicata da SQL Server durante l'ottimizzazione di una query che coinvolge le espressioni di tabella, risultando in un piano che interagisce direttamente con le tabelle di base sottostanti. Tieni presente che l'ottimizzatore può scegliere di utilizzare le tabelle di lavoro per mantenere i set di risultati intermedi se ha senso farlo per motivi di prestazioni o altri, come la protezione di Halloween. Quando lo fa, vedrai gli operatori Spool o Index Spool nel piano. Tuttavia, tali scelte non sono correlate all'uso delle espressioni di tabella nella query.
CTE ricorsivi
Esistono un paio di eccezioni in cui SQL Server persiste i dati dell'espressione della tabella. Uno è l'uso di viste indicizzate. Se si crea un indice cluster in una vista, SQL Server mantiene il set di risultati della query interna nell'indice cluster della vista e lo mantiene sincronizzato con eventuali modifiche nelle tabelle di base sottostanti. L'altra eccezione è quando si utilizzano query ricorsive. SQL Server deve mantenere i set di risultati intermedi delle query anchor e ricorsive in uno spool in modo che possa accedere al set di risultati dell'ultimo round rappresentato dal riferimento ricorsivo al nome CTE ogni volta che viene eseguito il membro ricorsivo.
Per dimostrarlo utilizzerò una delle query ricorsive della Parte 6 della serie.
Utilizzare il codice seguente per creare la tabella Employees nel database tempdb, popolarla con dati di esempio e creare un indice di supporto:
SET NOCOUNT ON; USE tempdb; DROP TABLE IF EXISTS dbo.Employees; GO CREATE TABLE dbo.Employees ( empid INT NOT NULL CONSTRAINT PK_Employees PRIMARY KEY, mgrid INT NULL CONSTRAINT FK_Employees_Employees REFERENCES dbo.Employees, empname VARCHAR(25) NOT NULL, salary MONEY NOT NULL, CHECK (empid <> mgrid) ); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(1, NULL, 'David' , $10000.00), (2, 1, 'Eitan' , $7000.00), (3, 1, 'Ina' , $7500.00), (4, 2, 'Seraph' , $5000.00), (5, 2, 'Jiru' , $5500.00), (6, 2, 'Steve' , $4500.00), (7, 3, 'Aaron' , $5000.00), (8, 5, 'Lilach' , $3500.00), (9, 7, 'Rita' , $3000.00), (10, 5, 'Sean' , $3000.00), (11, 7, 'Gabriel', $3000.00), (12, 9, 'Emilia' , $2000.00), (13, 9, 'Michael', $2000.00), (14, 9, 'Didi' , $1500.00); CREATE UNIQUE INDEX idx_unc_mgrid_empid ON dbo.Employees(mgrid, empid) INCLUDE(empname, salary); GO
Ho utilizzato il seguente CTE ricorsivo per restituire tutti i subordinati di un gestore radice di sottoalbero di input, utilizzando l'impiegato 3 come gestore di input in questo esempio:
DECLARE @root AS INT = 3; WITH C AS ( SELECT empid, mgrid, empname FROM dbo.Employees WHERE empid = @root UNION ALL SELECT S.empid, S.mgrid, S.empname FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid = M.empid ) SELECT empid, mgrid, empname FROM C;
Il piano per questa query (che chiameremo Query 6) è mostrato nella Figura 6.
Figura 6:piano di esecuzione per la query 6
Osservare che la prima cosa che accade nel piano, a destra del nodo radice SELECT, è la creazione di una tabella di lavoro basata su B-tree rappresentata dall'operatore Index Spool. La parte superiore del piano gestisce la logica del membro di ancoraggio. Estrae le righe dei dipendenti di input dall'indice cluster su Dipendenti e le scrive nello spool. La parte inferiore del piano rappresenta la logica del membro ricorsivo. Viene eseguito ripetutamente finché non restituisce un set di risultati vuoto. L'input esterno all'operatore Nested Loops ottiene i gestori del round precedente dallo spool (operatore Table Spool). L'input interno utilizza un operatore Index Seek rispetto a un indice non cluster creato su Employees(mgrid, empid) per ottenere i subordinati diretti dei manager dal round precedente. Anche il set di risultati di ogni esecuzione della parte inferiore del piano viene scritto nello spool dell'indice. Si noti che in tutto sono state scritte 7 righe nello spool. Uno restituito dal membro di ancoraggio e altri 6 restituiti da tutte le esecuzioni del membro ricorsivo.
Per inciso, è interessante notare come il piano gestisca il limite di maxrecursione predefinito, che è 100. Osservare che l'operatore di calcolo scalare inferiore continua ad aumentare di 1 un contatore interno chiamato Expr1011 ad ogni esecuzione del membro ricorsivo. Quindi, l'operatore Assert imposta un flag su zero se questo contatore supera 100. In questo caso SQL Server interrompe l'esecuzione della query e genera un errore.
Quando non insistere
Tornando ai CTE non ricorsivi, che normalmente non vengono mantenuti, sta a te capire da una prospettiva di ottimizzazione quando è una buona cosa usarli rispetto a strumenti di persistenza effettivi come tabelle temporanee e variabili di tabella. Esaminerò un paio di esempi per dimostrare quando ogni approccio è più ottimale.
Iniziamo con un esempio in cui i CTE fanno meglio delle tabelle temporanee. Questo è spesso il caso quando non si hanno più valutazioni dello stesso CTE, ma forse solo una soluzione modulare in cui ogni CTE viene valutato una sola volta. Il codice seguente (lo chiameremo Query 7) interroga la tabella Orders nel database Performance, che ha 1.000.000 di righe, per restituire gli anni dell'ordine in cui più di 70 clienti distinti hanno effettuato ordini:
USE PerformanceV5; WITH C1 AS ( SELECT YEAR(orderdate) AS orderyear, custid FROM dbo.Orders ), C2 AS ( SELECT orderyear, COUNT(DISTINCT custid) AS numcusts FROM C1 GROUP BY orderyear ) SELECT orderyear, numcusts FROM C2 WHERE numcusts > 70;
Questa query genera il seguente output:
orderyear numcusts ----------- ----------- 2015 992 2017 20000 2018 20000 2019 20000 2016 20000
Ho eseguito questo codice utilizzando SQL Server 2019 Developer Edition e ho ottenuto il piano mostrato nella Figura 7.
Figura 7:Piano di esecuzione per la query 7
Si noti che l'annullamento dell'annidamento del CTE ha portato a un piano che estrae i dati da un indice nella tabella Ordini e non comporta alcuno spooling del set di risultati della query interna del CTE. Ho ottenuto i seguenti numeri di prestazioni durante l'esecuzione di questa query sul mio computer:
duration: 265 ms, CPU: 828 ms, reads: 3970, writes: 0
Ora proviamo una soluzione che utilizza tabelle temporanee invece di CTE (la chiameremo Soluzione 8), in questo modo:
SELECT YEAR(orderdate) AS orderyear, custid INTO #T1 FROM dbo.Orders; SELECT orderyear, COUNT(DISTINCT custid) AS numcusts INTO #T2 FROM #T1 GROUP BY orderyear; SELECT orderyear, numcusts FROM #T2 WHERE numcusts > 70; DROP TABLE #T1, #T2;
I piani per questa soluzione sono mostrati nella Figura 8.
Figura 8:Piani per la Soluzione 8
Notare gli operatori di inserimento tabella che scrivono i set di risultati nelle tabelle temporanee #T1 e #T2. Il primo è particolarmente costoso poiché scrive 1.000.000 di righe su #T1. Ecco i numeri delle prestazioni che ho ottenuto per questa esecuzione:
duration: 454 ms, CPU: 1517 ms, reads: 14359, writes: 359
Come puoi vedere, la soluzione con i CTE è molto più ottimale.
Quando insistere
Quindi è sempre preferibile una soluzione modulare che prevede una sola valutazione di ogni CTE rispetto all'utilizzo di tabelle temporanee? Non necessariamente. Nelle soluzioni basate su CTE che comportano molti passaggi e danno luogo a piani elaborati in cui l'ottimizzatore deve applicare molte stime di cardinalità in molti punti diversi del piano, potresti ritrovarti con imprecisioni accumulate che si traducono in scelte non ottimali. Una delle tecniche per tentare di affrontare tali casi consiste nel persistere alcuni set di risultati intermedi in tabelle temporanee e persino creare indici su di essi se necessario, dando all'ottimizzatore un nuovo inizio con nuove statistiche, aumentando la probabilità di stime di cardinalità di migliore qualità che si spera che porti a scelte più ottimali. Se questo è meglio di una soluzione che non utilizza tabelle temporanee è qualcosa che dovrai testare. A volte ne varrà la pena il compromesso di costi aggiuntivi per la persistenza di set di risultati intermedi al fine di ottenere stime di cardinalità di qualità migliore.
Un altro caso tipico in cui l'utilizzo di tabelle temporanee è l'approccio preferito è quando la soluzione basata su CTE ha più valutazioni dello stesso CTE e la query interna del CTE è piuttosto costosa. Considera la seguente soluzione basata su CTE (la chiameremo Query 9), che corrisponde a ogni anno e mese dell'ordine a un anno e al mese dell'ordine diversi con il conteggio degli ordini più vicino:
WITH OrdCount AS ( SELECT YEAR(orderdate) AS orderyear, MONTH(orderdate) AS ordermonth, COUNT(*) AS numorders FROM dbo.Orders GROUP BY YEAR(orderdate), MONTH(orderdate) ) SELECT O1.orderyear, O1.ordermonth, O1.numorders, O2.orderyear AS orderyear2, O2.ordermonth AS ordermonth2, O2.numorders AS numorders2 FROM OrdCount AS O1 CROSS APPLY ( SELECT TOP (1) O2.orderyear, O2.ordermonth, O2.numorders FROM OrdCount AS O2 WHERE O2.orderyear <> O1.orderyear OR O2.ordermonth <> O1.ordermonth ORDER BY ABS(O1.numorders - O2.numorders), O2.orderyear, O2.ordermonth ) AS O2;
Questa query genera il seguente output:
orderyear ordermonth numorders orderyear2 ordermonth2 numorders2 ----------- ----------- ----------- ----------- ----------- ----------- 2016 1 21262 2017 3 21267 2019 1 21227 2016 5 21229 2019 2 19145 2018 2 19125 2018 4 20561 2016 9 20554 2018 5 21209 2019 5 21210 2018 6 20515 2016 11 20513 2018 7 21194 2018 10 21197 2017 9 20542 2017 11 20539 2017 10 21234 2019 3 21235 2017 11 20539 2019 4 20537 2017 12 21183 2016 8 21185 2018 1 21241 2019 7 21238 2016 2 19844 2019 12 20184 2018 3 21222 2016 10 21222 2016 4 20526 2019 9 20527 2019 4 20537 2017 11 20539 2017 5 21203 2017 8 21199 2019 6 20531 2019 9 20527 2017 7 21217 2016 7 21218 2018 8 21283 2017 3 21267 2018 10 21197 2017 8 21199 2016 11 20513 2018 6 20515 2019 11 20494 2017 4 20498 2018 2 19125 2019 2 19145 2016 3 21211 2016 12 21212 2019 3 21235 2017 10 21234 2016 5 21229 2019 1 21227 2019 5 21210 2016 3 21211 2017 6 20551 2016 9 20554 2017 8 21199 2018 10 21197 2018 9 20487 2019 11 20494 2016 10 21222 2018 3 21222 2018 11 20575 2016 6 20571 2016 12 21212 2016 3 21211 2019 12 20184 2018 9 20487 2017 1 21223 2016 10 21222 2017 2 19174 2019 2 19145 2017 3 21267 2016 1 21262 2017 4 20498 2019 11 20494 2016 6 20571 2018 11 20575 2016 7 21218 2017 7 21217 2019 7 21238 2018 1 21241 2016 8 21185 2017 12 21183 2019 8 21189 2016 8 21185 2016 9 20554 2017 6 20551 2019 9 20527 2016 4 20526 2019 10 21254 2016 1 21262 2015 12 1018 2018 2 19125 2018 12 21225 2017 1 21223 (49 rows affected)
Il piano per la query 9 è mostrato nella Figura 9.
Figura 9:Piano di esecuzione per la query 9
La parte superiore del piano corrisponde all'istanza dell'OrdCount CTE alias O1. Questo riferimento si traduce in una valutazione del CTE OrdCount. Questa parte del piano estrae le righe da un indice nella tabella Ordini, le raggruppa per anno e mese e aggrega il conteggio degli ordini per gruppo, ottenendo 49 righe. La parte inferiore del piano corrisponde alla tabella derivata correlata O2, che viene applicata per riga da O1, quindi viene eseguita 49 volte. Ogni esecuzione interroga l'OrdCount CTE, e quindi si traduce in una valutazione separata della query interna del CTE. Puoi vedere che la parte inferiore del piano scansiona tutte le righe dall'indice su Ordini, raggruppa e le aggrega. In pratica ottieni un totale di 50 valutazioni del CTE, con il risultato di scansionare 50 volte le 1.000.000 di righe di Ordini, raggruppandole e aggregandole. Non sembra una soluzione molto efficiente. Ecco le misure delle prestazioni che ho ottenuto durante l'esecuzione di questa soluzione sul mio computer:
duration: 16 seconds, CPU: 56 seconds, reads: 130404, writes: 0
Dato che ci sono solo poche decine di mesi coinvolti, sarebbe molto più efficiente utilizzare una tabella temporanea per memorizzare il risultato di una singola attività che raggruppa e aggrega le righe di Ordini, e quindi avere sia gli input esterni che quelli interni di l'operatore APPLY interagisce con la tabella temporanea. Ecco la soluzione (la chiameremo Soluzione 10) utilizzando una tabella temporanea al posto del CTE:
SELECT YEAR(orderdate) AS orderyear, MONTH(orderdate) AS ordermonth, COUNT(*) AS numorders INTO #OrdCount FROM dbo.Orders GROUP BY YEAR(orderdate), MONTH(orderdate); SELECT O1.orderyear, O1.ordermonth, O1.numorders, O2.orderyear AS orderyear2, O2.ordermonth AS ordermonth2, O2.numorders AS numorders2 FROM #OrdCount AS O1 CROSS APPLY ( SELECT TOP (1) O2.orderyear, O2.ordermonth, O2.numorders FROM #OrdCount AS O2 WHERE O2.orderyear <> O1.orderyear OR O2.ordermonth <> O1.ordermonth ORDER BY ABS(O1.numorders - O2.numorders), O2.orderyear, O2.ordermonth ) AS O2; DROP TABLE #OrdCount;
Qui non ha molto senso indicizzare la tabella temporanea, poiché il filtro TOP si basa su un calcolo nella sua specifica di ordinamento, e quindi un ordinamento è inevitabile. Tuttavia, potrebbe benissimo essere che in altri casi, con altre soluzioni, sarebbe importante considerare anche l'indicizzazione delle tabelle temporanee. In ogni caso, il piano per questa soluzione è mostrato nella Figura 10.
Figura 10:piani di esecuzione per la Soluzione 10
Osservare nella pianta in alto come il sollevamento di carichi pesanti che comporta la scansione di 1.000.000 di righe, il raggruppamento e l'aggregazione, avviene solo una volta. 49 righe vengono scritte nella tabella temporanea #OrdCount, quindi il piano inferiore interagisce con la tabella temporanea sia per gli input esterni che per quelli interni dell'operatore Nested Loops, che gestisce la logica dell'operatore APPLY.
Ecco i numeri delle prestazioni che ho ottenuto per l'esecuzione di questa soluzione:
duration: 0.392 seconds, CPU: 0.5 seconds, reads: 3636, writes: 3
È più veloce in ordine di grandezza rispetto alla soluzione basata su CTE.
Cosa c'è dopo?
In questo articolo ho iniziato la trattazione delle considerazioni di ottimizzazione relative ai CTE. Ho mostrato che il processo di annullamento/sostituzione che avviene con le tabelle derivate funziona allo stesso modo con i CTE. Ho anche discusso del fatto che i CTE non ricorsivi non vengono persistenti e ho spiegato che quando la persistenza è un fattore importante per le prestazioni della tua soluzione, devi gestirlo da solo utilizzando strumenti come tabelle temporanee e variabili di tabella. Il mese prossimo continuerò la discussione trattando aspetti aggiuntivi dell'ottimizzazione CTE.