Database
 sql >> Database >  >> RDS >> Database

Fondamenti di espressioni tabellari, Parte 5 – CTE, considerazioni logiche

Questo articolo è la quinta parte di una serie sulle espressioni di tabella. Nella parte 1 ho fornito lo sfondo alle espressioni della tabella. Nella Parte 2, Parte 3 e Parte 4, ho trattato sia gli aspetti logici che quelli di ottimizzazione delle tabelle derivate. Questo mese inizio la copertura delle espressioni di tabella comuni (CTE). Come con le tabelle derivate, affronterò prima il trattamento logico dei CTE e in futuro passerò alle considerazioni sull'ottimizzazione.

Nei miei esempi userò un database di esempio chiamato TSQLV5. Puoi trovare lo script che lo crea e lo popola qui e il suo diagramma ER qui.

CTE

Iniziamo con il termine espressione di tabella comune . Né questo termine, né il suo acronimo CTE, compaiono nelle specifiche dello standard SQL ISO/IEC. Quindi potrebbe essere che il termine abbia avuto origine in uno dei prodotti di database e successivamente adottato da alcuni degli altri fornitori di database. Puoi trovarlo nella documentazione di Microsoft SQL Server e del database SQL di Azure. T-SQL lo supporta a partire da SQL Server 2005. Lo standard utilizza il termine espressione di query per rappresentare un'espressione che definisce uno o più CTE, inclusa la query esterna. Usa il termine con elemento elenco per rappresentare ciò che T-SQL chiama CTE. A breve fornirò la sintassi per un'espressione di query.

La fonte del termine a parte, espressione di tabella comune o CTE , è il termine comunemente usato dai professionisti di T-SQL per la struttura oggetto di questo articolo. Quindi, prima, analizziamo se si tratta di un termine appropriato. Abbiamo già concluso che il termine espressione di tabella è appropriato per un'espressione che restituisce concettualmente una tabella. Tabelle derivate, CTE, viste e funzioni con valori di tabelle inline sono tutti i tipi di espressioni di tabelle con nome che T-SQL supporta. Quindi, l'espressione tabella parte di espressione di tabella comune sembra certamente appropriato. Per quanto riguarda il comune parte del termine, probabilmente ha a che fare con uno dei vantaggi di progettazione dei CTE rispetto alle tabelle derivate. Ricorda che non puoi riutilizzare il nome della tabella derivata (o più precisamente il nome della variabile di intervallo) più di una volta nella query esterna. Al contrario, il nome CTE può essere utilizzato più volte nella query esterna. In altre parole, il nome CTE è comune alla query esterna. Ovviamente, dimostrerò questo aspetto del design in questo articolo.

I CTE offrono vantaggi simili alle tabelle derivate, incluso lo sviluppo di soluzioni modulari, il riutilizzo di alias di colonna, l'interazione indiretta con le funzioni della finestra in clausole che normalmente non le consentono, il supporto di modifiche che si basano indirettamente su TOP o OFFSET FETCH con la specifica dell'ordine, e altri. Ma ci sono alcuni vantaggi di progettazione rispetto alle tabelle derivate, che tratterò in dettaglio dopo aver fornito la sintassi per la struttura.

Sintassi

Ecco la sintassi dello standard per un'espressione di query:

7.17


Funzione
Specifica una tabella.


Formato
::=
[ ]
[ ] [ ] [ ]
::=CON [ RICORSIVA ]
::= [ { }… ]
::=
[ ]
AS [ ]
::=
::=

| UNION [ TUTTI | DISTINCT ]
[ ]
| EXCEPT [ TUTTI | DISTINCT ]
[ ]
::=

| INTERSECA [ TUTTI | DISTINCT ]
[ ]
::=

|
[ ] [ ] [ ]

::=
| |
::=TABLE
::=
CORRISPONDENTE [ BY ]
::=
::=ORDER BY
::=OFFSET { RIGA | ROWS }
::=
FETCH { FIRST | AVANTI } [ ] { RIGA | RIGHE } { SOLO | CON PARAGGI }
::=

|
::=
::=
::= PERCENTUALE


7.18


Funzione
Specificare la generazione di informazioni sull'ordinamento e sul rilevamento del ciclo nel risultato di espressioni di query ricorsive.


Formato
::=
| |
::=
SEARCH SET
::=
DEPTH FIRST BY | BREADTH FIRST BY
::=
::=
CYCLE SET TO
PREDEFINITO USING
::= [ { }… ]
::=
::=
::=
::=
::=


7.3


Funzione
Specificare un insieme di s da costruire in una tabella.


Formato
::=VALUES
::=
[ { }… ]
::=
VALUES
::=

[ { }… ]

Il termine standard espressione di query rappresenta un'espressione che coinvolge una clausola WITH, un con elenco , che è composto da uno o più con elementi di elenco e una query esterna. T-SQL si riferisce allo standard con elemento elenco come CTE.

T-SQL non supporta tutti gli elementi di sintassi standard. Ad esempio, non supporta alcuni degli elementi di query ricorsivi più avanzati che consentono di controllare la direzione della ricerca e gestire i cicli in una struttura grafica. Le query ricorsive sono al centro dell'articolo del prossimo mese.

Ecco la sintassi T-SQL per una query semplificata su un CTE:

WITH < table name > [ (< target columns >) ] AS
(
  < table expression >
)
SELECT < select list >
FROM < table name >;

Ecco un esempio per una semplice query su un CTE che rappresenta i clienti USA:

WITH UC AS
(
  SELECT custid, companyname
  FROM Sales.Customers
  WHERE country = N'USA'
)
SELECT custid, companyname
FROM UC;

Troverai le stesse tre parti in una dichiarazione contro un CTE come faresti con una dichiarazione contro una tabella derivata:

  1. L'espressione della tabella (la query interna)
  2. Il nome assegnato all'espressione della tabella (il nome della variabile di intervallo)
  3. La query esterna

La differenza nel design dei CTE rispetto alle tabelle derivate è dove nel codice si trovano questi tre elementi. Con le tabelle derivate, la query interna viene nidificata all'interno della clausola FROM della query esterna e il nome dell'espressione della tabella viene assegnato dopo l'espressione della tabella stessa. Gli elementi sono in qualche modo intrecciati. Al contrario, con le CTE, il codice separa i tre elementi:prima si assegna il nome dell'espressione della tabella; secondo si specifica l'espressione della tabella, dall'inizio alla fine senza interruzioni; terzo si specifica la query esterna, dall'inizio alla fine senza interruzioni. Successivamente, in "Considerazioni sul design", spiegherò le implicazioni di queste differenze di design.

Una parola sui CTE e sull'uso di un punto e virgola come terminatore di istruzioni. Sfortunatamente, a differenza dell'SQL standard, T-SQL non ti obbliga a terminare tutte le istruzioni con un punto e virgola. Tuttavia, ci sono pochissimi casi in T-SQL in cui senza un terminatore il codice è ambiguo. In questi casi, la risoluzione è obbligatoria. Uno di questi casi riguarda il fatto che la clausola WITH viene utilizzata per molteplici scopi. Uno è definire un CTE, un altro è definire un suggerimento tabella per una query e ci sono alcuni casi d'uso aggiuntivi. Ad esempio, nell'istruzione seguente viene utilizzata la clausola WITH per forzare il livello di isolamento serializzabile con un suggerimento di tabella:

SELECT custid, country FROM Sales.Customers WITH (SERIALIZABLE);

Il potenziale di ambiguità è quando hai un'istruzione non terminata che precede una definizione CTE, nel qual caso il parser potrebbe non essere in grado di dire se la clausola WITH appartiene alla prima o alla seconda istruzione. Ecco un esempio che lo dimostra:

SELECT custid, country FROM Sales.Customers
 
WITH UC AS
(
  SELECT custid, companyname
  FROM Sales.Customers
  WHERE country = N'USA'
)
SELECT custid, companyname
FROM UC

Qui il parser non può dire se la clausola WITH deve essere utilizzata per definire un suggerimento di tabella per la tabella Customers nella prima istruzione o avviare una definizione CTE. Viene visualizzato il seguente errore:

Msg 336, livello 15, stato 1, riga 159
Sintassi errata vicino a 'UC'. Se questa deve essere un'espressione di tabella comune, è necessario terminare in modo esplicito l'istruzione precedente con un punto e virgola.

La soluzione è ovviamente terminare l'istruzione che precede la definizione CTE, ma come best practice, dovresti davvero terminare tutte le tue affermazioni:

SELECT custid, country FROM Sales.Customers;
 
WITH UC AS
(
  SELECT custid, companyname
  FROM Sales.Customers
  WHERE country = N'USA'
)
SELECT custid, companyname
FROM UC;

Potresti aver notato che alcune persone iniziano le loro definizioni CTE con un punto e virgola come pratica, in questo modo:

;WITH UC AS
(
  SELECT custid, companyname
  FROM Sales.Customers
  WHERE country = N'USA'
)
SELECT custid, companyname
FROM UC;

Il punto in questa pratica è ridurre il potenziale di errori futuri. Cosa succede se in un secondo momento qualcuno aggiunge un'istruzione non terminata subito prima della definizione CTE nello script e non si preoccupa di controllare lo script completo, piuttosto solo la sua affermazione? Il tuo punto e virgola subito prima che la clausola WITH diventi effettivamente il loro terminatore di istruzione. Puoi certamente vedere la praticità di questa pratica, ma è un po' innaturale. Ciò che è raccomandato, anche se più difficile da ottenere, è instillare buone pratiche di programmazione nell'organizzazione, inclusa la cessazione di tutte le dichiarazioni.

In termini di regole di sintassi che si applicano all'espressione di tabella utilizzata come query interna nella definizione CTE, sono le stesse che si applicano all'espressione di tabella utilizzata come query interna in una definizione di tabella derivata. Quelli sono:

  • Tutte le colonne dell'espressione della tabella devono avere nomi
  • Tutti i nomi di colonna dell'espressione di tabella devono essere univoci
  • Le righe dell'espressione della tabella non hanno ordine

Per i dettagli, vedere la sezione "Un'espressione di tabella è una tabella" nella Parte 2 della serie.

Considerazioni di progettazione

Se si esaminano sviluppatori T-SQL esperti per sapere se preferiscono utilizzare tabelle derivate o CTE, non tutti saranno d'accordo su quale sia il migliore. Naturalmente, persone diverse hanno preferenze di stile diverse. A volte uso tabelle derivate e talvolta CTE. È utile essere in grado di identificare consapevolmente le differenze di design del linguaggio specifico tra i due strumenti e scegliere in base alle tue priorità in una determinata soluzione. Con il tempo e l'esperienza, fai le tue scelte in modo più intuitivo.

Inoltre, è importante non confondere l'uso di espressioni di tabella e tabelle temporanee, ma questa è una discussione relativa alle prestazioni che affronterò in un prossimo articolo.

I CTE hanno funzionalità di query ricorsive e le tabelle derivate no. Quindi, se hai bisogno di fare affidamento su quelli, andresti naturalmente con i CTE. Le query ricorsive sono al centro dell'articolo del prossimo mese.

Nella parte 2 ho spiegato che vedo l'annidamento di tabelle derivate come un'aggiunta di complessità al codice, poiché rende difficile seguire la logica. Ho fornito il seguente esempio, identificando gli anni dell'ordine in cui più di 70 clienti hanno effettuato ordini:

SELECT orderyear, numcusts
FROM ( SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
         FROM ( SELECT YEAR(orderdate) AS orderyear, custid
                FROM Sales.Orders ) AS D1
         GROUP BY orderyear ) AS D2
  WHERE numcusts > 70;

I CTE non supportano la nidificazione. Quindi, quando esamini o risolvi una soluzione basata su CTE, non ti perdi nella logica annidata. Invece di annidare, crei soluzioni più modulari definendo più CTE sotto la stessa istruzione WITH, separati da virgole. Ciascuno dei CTE si basa su una query scritta dall'inizio alla fine senza interruzioni. Lo vedo come una buona cosa dal punto di vista della chiarezza del codice e della manutenibilità.

Ecco una soluzione all'attività di cui sopra utilizzando CTE:

WITH C1 AS
(
  SELECT YEAR(orderdate) AS orderyear, custid
  FROM Sales.Orders
),
C2 AS
(
  SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
  FROM C1
  GROUP BY orderyear
)
SELECT orderyear, numcusts
FROM C2
WHERE numcusts > 70;

Mi piace di più la soluzione basata su CTE. Ma ancora una volta, chiedi agli sviluppatori esperti quale delle due soluzioni precedenti preferiscono e non saranno tutti d'accordo. Alcuni in realtà preferiscono la logica annidata e la possibilità di vedere tutto in un unico posto.

Un chiaro vantaggio dei CTE rispetto alle tabelle derivate è quando è necessario interagire con più istanze della stessa espressione di tabella nella soluzione. Ricorda il seguente esempio basato su tabelle derivate dalla Parte 2 della serie:

SELECT CUR.orderyear, CUR.numorders,
  CUR.numorders - PRV.numorders AS diff
FROM ( SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders
         FROM Sales.Orders
         GROUP BY YEAR(orderdate) ) AS CUR
    LEFT OUTER JOIN
       ( SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders
         FROM Sales.Orders
         GROUP BY YEAR(orderdate) ) AS PRV
      ON CUR.orderyear = PRV.orderyear + 1;

Questa soluzione restituisce gli anni degli ordini, i conteggi degli ordini per anno e la differenza tra i conteggi dell'anno corrente e dell'anno precedente. Sì, potresti farlo più facilmente con la funzione LAG, ma il mio obiettivo qui non è trovare il modo migliore per svolgere questo compito molto specifico. Uso questo esempio per illustrare alcuni aspetti della progettazione del linguaggio delle espressioni di tabelle con nome.

Il problema con questa soluzione è che non è possibile assegnare un nome a un'espressione di tabella e riutilizzarla nella stessa fase di elaborazione della query logica. Assegna a una tabella derivata il nome dell'espressione della tabella stessa nella clausola FROM. Se si definisce e si denomina una tabella derivata come primo input di un join, non è possibile riutilizzare anche il nome della tabella derivata come secondo input dello stesso join. Se hai bisogno di unire due istanze della stessa espressione di tabella, con le tabelle derivate non hai altra scelta che duplicare il codice. Questo è quello che hai fatto nell'esempio sopra. Al contrario, il nome CTE viene assegnato come primo elemento del codice tra i tre sopra citati (nome CTE, query interna, query esterna). In termini di elaborazione logica della query, quando si arriva alla query esterna, il nome CTE è già definito e disponibile. Ciò significa che puoi interagire con più istanze del nome CTE nella query esterna, in questo modo:

WITH OrdCount AS
(
  SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders
  FROM Sales.Orders
  GROUP BY YEAR(orderdate)
)
SELECT CUR.orderyear, CUR.numorders,
  CUR.numorders - PRV.numorders AS diff
FROM OrdCount AS CUR
  LEFT OUTER JOIN OrdCount AS PRV
    ON CUR.orderyear = PRV.orderyear + 1;

Questa soluzione presenta un chiaro vantaggio di programmabilità rispetto a quella basata su tabelle derivate in quanto non è necessario mantenere due copie della stessa espressione di tabella. C'è altro da dire al riguardo dal punto di vista dell'elaborazione fisica e confrontarlo con l'uso di tabelle temporanee, ma lo farò in un prossimo articolo incentrato sulle prestazioni.

Un vantaggio che il codice basato su tabelle derivate ha rispetto al codice basato su CTE ha a che fare con la proprietà di chiusura che dovrebbe possedere un'espressione di tabella. Ricorda che la proprietà di chiusura di un'espressione relazionale dice che sia gli input che l'output sono relazioni e che un'espressione relazionale può quindi essere utilizzata dove è prevista una relazione, come input per un'altra espressione relazionale. Allo stesso modo, un'espressione di tabella restituisce una tabella e dovrebbe essere disponibile come tabella di input per un'altra espressione di tabella. Questo vale per una query basata su tabelle derivate:puoi usarla dove è prevista una tabella. Ad esempio, puoi utilizzare una query basata su tabelle derivate come query interna di una definizione CTE, come nell'esempio seguente:

WITH C AS
(
  SELECT orderyear, numcusts
  FROM ( SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
         FROM ( SELECT YEAR(orderdate) AS orderyear, custid
                FROM Sales.Orders ) AS D1
         GROUP BY orderyear ) AS D2
  WHERE numcusts > 70
)
SELECT orderyear, numcusts
FROM C;

Tuttavia, lo stesso non vale per una query basata su CTE. Anche se concettualmente dovrebbe essere considerata un'espressione di tabella, non è possibile utilizzarla come query interna nelle definizioni di tabelle derivate, nelle sottoquery e negli stessi CTE. Ad esempio, il codice seguente non è valido in T-SQL:

SELECT orderyear, custid
FROM (WITH C1 AS
      (
        SELECT YEAR(orderdate) AS orderyear, custid
        FROM Sales.Orders
      ),
      C2 AS
      (
        SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
        FROM C1
        GROUP BY orderyear
      )
      SELECT orderyear, numcusts
      FROM C2
      WHERE numcusts > 70) AS D;

La buona notizia è che puoi utilizzare una query basata su CTE come query interna nelle viste e nelle funzioni con valori di tabella in linea, che tratterò in articoli futuri.

Inoltre, ricorda che puoi sempre definire un altro CTE in base all'ultima query, quindi fare in modo che la query più esterna interagisca con quel CTE:

WITH C1 AS
(
  SELECT YEAR(orderdate) AS orderyear, custid
  FROM Sales.Orders
),
C2 AS
(
  SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
  FROM C1
  GROUP BY orderyear
),
C3 AS
(
  SELECT orderyear, numcusts
  FROM C2
  WHERE numcusts &gt; 70
)
SELECT orderyear, numcusts
FROM C3;

Dal punto di vista della risoluzione dei problemi, come accennato, di solito trovo più facile seguire la logica del codice basata su CTE, rispetto al codice basato su tabelle derivate. Tuttavia, le soluzioni basate su tabelle derivate hanno il vantaggio di poter evidenziare qualsiasi livello di annidamento ed eseguirlo in modo indipendente, come mostrato nella Figura 1.

Figura 1:può evidenziare ed eseguire parte del codice con tabelle derivate

Con i CTE le cose sono più complicate. Affinché il codice che coinvolge le CTE sia eseguibile, deve iniziare con una clausola WITH, seguita da una o più espressioni di tabella tra parentesi denominate separate da virgole, seguite da una query senza parentesi senza virgola precedente. Sei in grado di evidenziare ed eseguire qualsiasi query interna che sia veramente autonoma, così come il codice completo della soluzione; tuttavia, non è possibile evidenziare ed eseguire correttamente qualsiasi altra parte intermedia della soluzione. Ad esempio, la Figura 2 mostra un tentativo non riuscito di eseguire il codice che rappresenta C2.

Figura 2:impossibile evidenziare ed eseguire parte del codice con le CTE

Quindi, con i CTE, devi ricorrere a mezzi alquanto scomodi per poter risolvere un passaggio intermedio della soluzione. Ad esempio, una soluzione comune consiste nell'iniettare temporaneamente una query SELECT * FROM your_cte proprio sotto il CTE pertinente. Quindi evidenzi ed esegui il codice inclusa la query iniettata e, quando hai finito, elimini la query iniettata. La figura 3 mostra questa tecnica.

Figura 3:Inietta SELECT * sotto il CTE pertinente

Il problema è che ogni volta che apporti modifiche al codice, anche quelle minori temporanee come quelle sopra, c'è la possibilità che quando tenti di ripristinare il codice originale, finirai per introdurre un nuovo bug.

Un'altra opzione è modellare il codice in modo leggermente diverso, in modo tale che ogni definizione CTE non prima inizi con una riga di codice separata simile a questa:

, cte_name AS (

Quindi, ogni volta che vuoi eseguire una parte intermedia del codice fino a un determinato CTE, puoi farlo con modifiche minime al tuo codice. Usando un commento di riga si commenta solo quella riga di codice che corrisponde a quel CTE. Quindi evidenziare ed eseguire il codice fino a includere la query interna di CTE, che ora è considerata la query più esterna, come illustrato nella Figura 4.

Figura 4:riorganizzare la sintassi per abilitare il commento di una riga di codice

Se non sei soddisfatto di questo stile, hai ancora un'altra opzione. È possibile utilizzare un commento di blocco che inizia subito prima della virgola che precede il CTE di interesse e termina dopo la parentesi aperta, come illustrato nella Figura 5.

Figura 5:usa il commento del blocco

Si riduce alle preferenze personali. In genere utilizzo la tecnica di query SELECT * iniettata temporaneamente.

Costruttore di valori di tabella

C'è una certa limitazione nel supporto di T-SQL per i costruttori di valori di tabella rispetto allo standard. Se non hai familiarità con il costrutto, assicurati di controllare prima la Parte 2 della serie, dove lo descrivo in dettaglio. Mentre T-SQL consente di definire una tabella derivata in base a un costruttore di valori di tabella, non consente di definire un CTE in base a un costruttore di valori di tabella.

Ecco un esempio supportato che utilizza una tabella derivata:

SELECT custid, companyname, contractdate
FROM ( VALUES( 2, 'Cust 2', '20200212' ),
             ( 3, 'Cust 3', '20200118' ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate);

Sfortunatamente, un codice simile che utilizza un CTE non è supportato:

WITH MyCusts(custid, companyname, contractdate) AS
(
  VALUES( 2, 'Cust 2', '20200212' ),
        ( 3, 'Cust 3', '20200118' ),
        ( 5, 'Cust 5', '20200401' )
)
SELECT custid, companyname, contractdate
FROM MyCusts;

Questo codice genera il seguente errore:

Msg 156, livello 15, stato 1, riga 337
Sintassi errata vicino alla parola chiave 'VALUES'.

Ci sono un paio di soluzioni alternative, però. Uno consiste nell'utilizzare una query su una tabella derivata, che a sua volta è basata su un costruttore di valori di tabella, come query interna del CTE, in questo modo:

WITH MyCusts AS
(
  SELECT *
  FROM ( VALUES( 2, 'Cust 2', '20200212' ),
               ( 3, 'Cust 3', '20200118' ),
               ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate)
)
SELECT custid, companyname, contractdate
FROM MyCusts;

Un altro è ricorrere alla tecnica utilizzata dalle persone prima che i costruttori con valori di tabella venissero introdotti in T-SQL, utilizzando una serie di query FROMless separate da operatori UNION ALL, in questo modo:

WITH MyCusts(custid, companyname, contractdate) AS
(
            SELECT 2, 'Cust 2', '20200212'
  UNION ALL SELECT 3, 'Cust 3', '20200118'
  UNION ALL SELECT 5, 'Cust 5', '20200401'
)
SELECT custid, companyname, contractdate
FROM MyCusts;

Si noti che gli alias di colonna vengono assegnati subito dopo il nome CTE.

I due metodi vengono algebrizzati e ottimizzati allo stesso modo, quindi usa quello con cui ti senti più a tuo agio.

Produrre una sequenza di numeri

Uno strumento che uso abbastanza spesso nelle mie soluzioni è una tabella numerica ausiliaria. Un'opzione è creare una tabella numerica effettiva nel database e popolarla con una sequenza di dimensioni ragionevoli. Un altro è sviluppare una soluzione che produca una sequenza di numeri al volo. Per quest'ultima opzione, vuoi che gli input siano i delimitatori dell'intervallo desiderato (li chiameremo @low e @high ). Vuoi che la tua soluzione supporti gamme potenzialmente ampi. Ecco la mia soluzione per questo scopo, utilizzando CTE, con una richiesta per l'intervallo da 1001 a 1010 in questo esempio specifico:

DECLARE @low AS BIGINT = 1001, @high AS BIGINT = 1010;
 
WITH
  L0 AS ( SELECT 1 AS c FROM (VALUES(1),(1)) AS D(c) ),
  L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
  L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
  L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
  L4 AS ( SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B ),
  L5 AS ( SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B ),
  Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
            FROM L5 )
SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
FROM Nums
ORDER BY rownum;

Questo codice genera il seguente output:

n
-----
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010

Il primo CTE chiamato L0 si basa su un costruttore di valori di tabella con due righe. I valori effettivi sono insignificanti; l'importante è che abbia due righe. Quindi, c'è una sequenza di cinque CTE aggiuntivi denominati da L1 a L5, ciascuno dei quali applica un cross join tra due istanze del CTE precedente. Il codice seguente calcola il numero di righe potenzialmente generate da ciascuna delle CTE, dove @L è il numero di livello CTE:

DECLARE @L AS INT = 5;
 
SELECT POWER(2., POWER(2., @L));

Ecco i numeri che ottieni per ogni CTE:

CTE Cardinalità
L0 2
L1 4
L2 16
L3 256
L4 65.536
L5 4.294.967.296

Salire al livello 5 ti dà oltre quattro miliardi di righe. Questo dovrebbe essere sufficiente per qualsiasi caso d'uso pratico che mi viene in mente. Il passaggio successivo avviene nel CTE chiamato Nums. Utilizzare una funzione ROW_NUMBER per generare una sequenza di numeri interi che iniziano con 1 in base a un ordine non definito (ORDER BY (SELECT NULL)) e denominare la colonna del risultato rownum. Infine, la query esterna utilizza un filtro TOP basato sull'ordine rownum per filtrare tanti numeri quanto la cardinalità della sequenza desiderata (@high – @low + 1) e calcola il numero del risultato n come @low + rownum – 1.

Qui puoi davvero apprezzare la bellezza del design CTE e i risparmi che consente quando costruisci soluzioni in modo modulare. Infine, il processo di disnidificazione estrae 32 tabelle, ciascuna composta da due righe basate su costanti. Questo può essere visto chiaramente nel piano di esecuzione di questo codice, come mostrato nella Figura 6 usando SentryOne Plan Explorer.

Figura 6:piano per la generazione di query sequenza di numeri

Ciascun operatore Constant Scan rappresenta una tabella di costanti con due righe. Il fatto è che l'operatore Top è quello che richiede quelle righe e va in cortocircuito dopo aver ottenuto il numero desiderato. Nota le 10 righe indicate sopra la freccia che scorre nell'operatore Top.

So che l'obiettivo di questo articolo è il trattamento concettuale dei CTE e non considerazioni fisiche/prestazioni, ma guardando il piano puoi davvero apprezzare la brevità del codice rispetto alla prolissità di ciò che si traduce dietro le quinte.

Utilizzando le tabelle derivate, puoi effettivamente scrivere una soluzione che sostituisce ogni riferimento CTE con la query sottostante che rappresenta. Quello che ottieni è abbastanza spaventoso:

DECLARE @low AS BIGINT = 1001, @high AS BIGINT = 1010;
 
SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
FROM ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
       FROM ( SELECT 1 AS C
              FROM ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
 
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D7
                CROSS JOIN
                   ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D8 ) AS D9
         CROSS JOIN
            ( SELECT 1 AS C
              FROM ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D7
                CROSS JOIN
                   ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D8 ) AS D10 ) AS Nums
ORDER BY rownum;

Obviously, you don’t want to write a solution like this, but it’s a good way to illustrate what SQL Server does behind the scenes with your CTE code.

If you were really planning to write a solution based on derived tables, instead of using the above nested approach, you’d be better off simplifying the logic to a single query with 31 cross joins between 32 table value constructors, each based on two rows, like so:

DECLARE @low AS BIGINT = 1001, @high AS BIGINT = 1010;
 
SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
FROM ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
       FROM         (VALUES(1),(1)) AS D01(c)
         CROSS JOIN (VALUES(1),(1)) AS D02(c)
         CROSS JOIN (VALUES(1),(1)) AS D03(c)
         CROSS JOIN (VALUES(1),(1)) AS D04(c)
         CROSS JOIN (VALUES(1),(1)) AS D05(c)
         CROSS JOIN (VALUES(1),(1)) AS D06(c)
         CROSS JOIN (VALUES(1),(1)) AS D07(c)
         CROSS JOIN (VALUES(1),(1)) AS D08(c)
         CROSS JOIN (VALUES(1),(1)) AS D09(c)
         CROSS JOIN (VALUES(1),(1)) AS D10(c)
         CROSS JOIN (VALUES(1),(1)) AS D11(c)
         CROSS JOIN (VALUES(1),(1)) AS D12(c)
         CROSS JOIN (VALUES(1),(1)) AS D13(c)
         CROSS JOIN (VALUES(1),(1)) AS D14(c)
         CROSS JOIN (VALUES(1),(1)) AS D15(c)
         CROSS JOIN (VALUES(1),(1)) AS D16(c)
         CROSS JOIN (VALUES(1),(1)) AS D17(c)
         CROSS JOIN (VALUES(1),(1)) AS D18(c)
         CROSS JOIN (VALUES(1),(1)) AS D19(c)
         CROSS JOIN (VALUES(1),(1)) AS D20(c)
         CROSS JOIN (VALUES(1),(1)) AS D21(c)
         CROSS JOIN (VALUES(1),(1)) AS D22(c)
         CROSS JOIN (VALUES(1),(1)) AS D23(c)
         CROSS JOIN (VALUES(1),(1)) AS D24(c)
         CROSS JOIN (VALUES(1),(1)) AS D25(c)
         CROSS JOIN (VALUES(1),(1)) AS D26(c)
         CROSS JOIN (VALUES(1),(1)) AS D27(c)
         CROSS JOIN (VALUES(1),(1)) AS D28(c)
         CROSS JOIN (VALUES(1),(1)) AS D29(c)
         CROSS JOIN (VALUES(1),(1)) AS D30(c)
         CROSS JOIN (VALUES(1),(1)) AS D31(c)
         CROSS JOIN (VALUES(1),(1)) AS D32(c) ) AS Nums
ORDER BY rownum;

Still, the solution based on CTEs is obviously significantly simpler. The plans are identical.

Used in modification statements

CTEs can be used as the source and target tables in INSERT, UPDATE, DELETE and MERGE statements. They cannot be used in the TRUNCATE statement.

The syntax is pretty straightforward. You start the statement as usual with a WITH clause, followed by one or more CTEs separated by commas. Then you specify the outer modification statement, which interacts with the CTEs that were defined under the WITH clause as the source tables, target table, or both. Just like I explained in Part 2 about derived tables, also with CTEs what really gets modified is the underlying base table that the table expression uses. I’ll show a couple of examples using DELETE and UPDATE statements, but remember that you can use CTEs in MERGE and INSERT statements as well.

Here’s the general syntax of a DELETE statement against a CTE:

WITH < table name > [ (< target columns >) ] AS
(
  < table expression >
)
DELETE [ FROM ] <table name>
[ WHERE <filter predicate> ];

As an example (don’t actually run it), the following code deletes the 10 oldest orders:

WITH OldestOrders AS
(
  SELECT TOP (10) *
  FROM Sales.Orders
  ORDER BY orderdate, orderid
)
DELETE FROM OldestOrders;

Here’s the general syntax of an UPDATE statement against a CTE:

WITH < table name > [ (< target columns >) ] AS
(
  < table expression >
)
UPDATE <table name>
  SET <assignments>
[ WHERE <filter predicate> ];

As an example, the following code updates the 10 oldest unshipped orders that have an overdue required date, increasing the required date to 10 days from today:

BEGIN TRAN;
 
WITH OldestUnshippedOrders AS
(
  SELECT TOP (10) orderid, requireddate,
    DATEADD(day, 10, CAST(SYSDATETIME() AS DATE)) AS newrequireddate
  FROM Sales.Orders
  WHERE shippeddate IS NULL
    AND requireddate &lt; CAST(SYSDATETIME() AS DATE)
  ORDER BY orderdate, orderid
)
UPDATE OldestUnshippedOrders
  SET requireddate = newrequireddate
    OUTPUT
      inserted.orderid,
      deleted.requireddate AS oldrequireddate,
      inserted.requireddate AS newrequireddate;
 
ROLLBACK TRAN;

The code applies the update in a transaction that it then rolls back so that the change won’t stick.

This code generates the following output, showing both the old and the new required dates:

orderid     oldrequireddate newrequireddate
----------- --------------- ---------------
11008       2019-05-06      2020-07-16
11019       2019-05-11      2020-07-16
11039       2019-05-19      2020-07-16
11040       2019-05-20      2020-07-16
11045       2019-05-21      2020-07-16
11051       2019-05-25      2020-07-16
11054       2019-05-26      2020-07-16
11058       2019-05-27      2020-07-16
11059       2019-06-10      2020-07-16
11061       2019-06-11      2020-07-16

(10 rows affected)

Of course you will get a different new required date based on when you run this code.

Riepilogo

I like CTEs. They have a few advantages compared to derived tables. Instead of nesting the code, you define multiple CTEs separated by commas, typically leading to a more modular solution that is easier to review and maintain. Also, you can have multiple references to the same CTE name in the outer statement, so you don’t need to repeat the inner table expression’s code. However, unlike derived tables, CTEs cannot be defined directly based on a table value constructor, and you cannot highlight and execute some of the intermediate parts of the code. The following table summarizes the differences between derived tables and CTEs:

Item Derived table CTE
Supports nesting Yes No
Supports multiple references No Yes
Supports table value constructor Yes No
Can highlight and run part of code Yes No
Supports recursion No Yes

As the last item says, derived tables do not support recursive capabilities, whereas CTEs do. Recursive queries are the focus of next month’s article.