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

Fondamenti di espressioni di tabella, Parte 2 – Tabelle derivate, considerazioni logiche

Il mese scorso ho fornito uno sfondo per le espressioni di tabella in T-SQL. Ho spiegato il contesto dalla teoria relazionale e dallo standard SQL. Ho spiegato come una tabella in SQL sia un tentativo di rappresentare una relazione dalla teoria relazionale. Ho anche spiegato che un'espressione relazionale è un'espressione che opera su una o più relazioni come input e risulta in una relazione. Allo stesso modo, in SQL, un'espressione di tabella è un'espressione che opera su una o più tabelle di input e risulta in una tabella. L'espressione può essere una query, ma non deve esserlo. Ad esempio, l'espressione può essere un costruttore di valori di tabella, come spiegherò più avanti in questo articolo. Ho anche spiegato che in questa serie, mi concentro su quattro tipi specifici di espressioni di tabelle con nome che T-SQL supporta:tabelle derivate, espressioni di tabelle comuni (CTE), viste e funzioni con valori di tabella inline (TVF).

Se hai lavorato con T-SQL per un po' di tempo, probabilmente ti sei imbattuto in un bel po' di casi in cui dovevi usare espressioni di tabella, o era in qualche modo più conveniente rispetto a soluzioni alternative che non le usano. Di seguito sono riportati solo alcuni esempi di casi d'uso che vengono in mente:

  • Crea una soluzione modulare suddividendo le attività complesse in passaggi, ognuno rappresentato da un'espressione di tabella diversa.
  • Mescolare i risultati di query raggruppate e dettagli, nel caso in cui tu decida di non utilizzare le funzioni della finestra per questo scopo.
  • Elaborazione logica delle query gestisce le clausole di query nel seguente ordine:FROM>WHERE>GROUP BY>HAVING>SELECT>ORDER BY. Di conseguenza, nello stesso livello di nidificazione, gli alias di colonna definiti nella clausola SELECT sono disponibili solo per la clausola ORDER BY. Non sono disponibili per il resto delle clausole di query. Con le espressioni di tabella puoi riutilizzare gli alias che definisci in una query interna in qualsiasi clausola della query esterna, evitando in questo modo la ripetizione di espressioni lunghe/complesse.
  • Le funzioni della finestra possono apparire solo nelle clausole SELECT e ORDER BY di una query. Con le espressioni di tabella, puoi assegnare un alias a un'espressione basata su una funzione della finestra e quindi utilizzare quell'alias in una query rispetto all'espressione di tabella.
  • Un operatore PIVOT comprende tre elementi:raggruppamento, diffusione e aggregazione. Questo operatore identifica l'elemento di raggruppamento implicitamente mediante eliminazione. Usando un'espressione di tabella, puoi proiettare esattamente i tre elementi che dovrebbero essere coinvolti e fare in modo che la query esterna utilizzi l'espressione di tabella come tabella di input dell'operatore PIVOT, controllando così quale elemento è l'elemento di raggruppamento.
  • Le modifiche con TOP non supportano una clausola ORDER BY. Puoi controllare quali righe vengono scelte indirettamente definendo un'espressione di tabella basata su una query SELECT con il filtro TOP o OFFSET-FETCH e una clausola ORDER BY e applicare la modifica all'espressione di tabella.

Questo è ben lungi dall'essere un elenco esaustivo. Dimostrerò alcuni dei casi d'uso di cui sopra e altri in questa serie. Volevo solo menzionare qui alcuni casi d'uso per illustrare quanto siano importanti le espressioni di tabella nel nostro codice T-SQL e perché vale la pena investire nella comprensione dei loro fondamenti.

Nell'articolo di questo mese mi concentro specificamente sul trattamento logico delle tabelle derivate.

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.

Tabelle derivate

Il termine tabella derivata viene utilizzato in SQL e T-SQL con più di un significato. Quindi prima voglio chiarire a quale mi riferisco in questo articolo. Mi riferisco a un costrutto di linguaggio specifico che definisci in genere, ma non solo, nella clausola FROM di una query esterna. A breve fornirò la sintassi per questo costrutto.

L'uso più generale del termine tabella derivata in SQL è la controparte di una relazione derivata dalla teoria relazionale. Una relazione derivata è una relazione di risultato derivata da una o più relazioni di base di input, applicando operatori relazionali dell'algebra relazionale come proiezione, intersezione e altri a quelle relazioni di base. Allo stesso modo, in senso generale, una tabella derivata in SQL è una tabella di risultati derivata da una o più tabelle di base, valutando le espressioni rispetto a quelle tabelle di base di input.

Per inciso, ho verificato come lo standard SQL definisce una tabella di base e mi sono subito dispiaciuto di aver disturbato.

4.15.2 Tabelle di base

Una tabella di base può essere una tabella di base persistente o una tabella temporanea.

Una tabella di base persistente è una normale tabella di base persistente o una tabella con versione di sistema.

Una normale tabella di base può essere una normale tabella di base persistente o una tabella temporanea."

Aggiunto qui senza ulteriori commenti...

In T-SQL, puoi creare una tabella di base con un'istruzione CREATE TABLE, ma ci sono altre opzioni, ad esempio SELECT INTO e DECLARE @T AS TABLE.

Ecco la definizione dello standard per le tabelle derivate in senso generale:

4.15.3 Tabelle derivate

Una tabella derivata è una tabella derivata direttamente o indirettamente da una o più altre tabelle dalla valutazione di un'espressione, ad esempio una , , o . Una può contenere una facoltativa. L'ordinamento delle righe della tabella specificato dalla è garantito solo per l' che contiene immediatamente la .”

Ci sono un paio di cose interessanti da notare qui sulle tabelle derivate in senso generale. Uno ha a che fare con il commento sull'ordinazione. Arriverò a questo più avanti nell'articolo. Un altro è che una tabella derivata in SQL può essere un'espressione di tabella autonoma valida, ma non deve esserlo. Ad esempio, la seguente espressione rappresenta una tabella derivata e è considerata anche un'espressione di tabella autonoma valida (puoi eseguirla):

SELECT custid, companyname
FROM Sales.Customers
WHERE country = N'USA'

Al contrario, l'espressione seguente rappresenta una tabella derivata, ma non lo è un'espressione di tabella autonoma valida:

T1 INNER JOIN T2
  ON T1.keycol = T2.keycol

T-SQL supporta numerosi operatori di tabella che producono una tabella derivata, ma non sono supportati come espressioni autonome. Questi sono:JOIN, PIVOT, UNPIVOT e APPLY. Hai bisogno di una clausola per farli funzionare all'interno (in genere FROM, ma anche la clausola USING dell'istruzione MERGE) e una query host.

Da qui in poi, userò il termine tabella derivata per descrivere un costrutto linguistico più specifico e non nel senso generale descritto sopra.

Sintassi

Una tabella derivata può essere definita come parte di un'istruzione SELECT esterna nella relativa clausola FROM. Può anche essere definito come parte delle istruzioni DELETE e UPDATE nella loro clausola FROM e come parte di un'istruzione MERGE nella relativa clausola USING. Fornirò maggiori dettagli sulla sintassi quando viene utilizzata nelle istruzioni di modifica più avanti in questo articolo.

Ecco la sintassi per una query SELECT semplificata su una tabella derivata:

SELECT
DA ( ) [ AS ]
[ () ];

La definizione della tabella derivata viene visualizzata dove normalmente può essere visualizzata una tabella di base, nella clausola FROM della query esterna. Può essere un input per un operatore di tabella come JOIN, APPLY, PIVOT e UNPIVOT. Quando viene utilizzata come input corretto per un operatore APPLY, la parte

della tabella derivata può avere correlazioni alle colonne di una tabella esterna (ulteriori informazioni su questo in un articolo futuro dedicato nella serie). In caso contrario, l'espressione della tabella deve essere autonoma.

L'istruzione esterna può avere tutti i consueti elementi di interrogazione. In un caso di istruzione SELECT:WHERE, GROUP BY, HAVING, ORDER BY e, come menzionato, operatori di tabella nella clausola FROM.

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

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

Questa query genera il seguente output:

custid  companyname
------- ---------------
32      Customer YSIQX
36      Customer LVJSO
43      Customer UISOJ
45      Customer QXPPT
48      Customer DVFMB
55      Customer KZQZT
65      Customer NYUHS
71      Customer LCOUJ
75      Customer XOJYP
77      Customer LCYBZ
78      Customer NLTYP
82      Customer EYHKM
89      Customer YBQTI

Ci sono tre parti principali da identificare in un'istruzione che coinvolge una definizione di tabella derivata:

  1. L'espressione della tabella (la query interna)
  2. Il nome della tabella derivata, o più precisamente, ciò che nella teoria relazionale è considerata una variabile di intervallo
  3. La dichiarazione esterna

L'espressione table dovrebbe rappresentare una tabella e, come tale, deve soddisfare determinati requisiti che una normale query non deve necessariamente soddisfare. Fornirò i dettagli a breve nella sezione "Un'espressione di tabella è una tabella".

Per quanto riguarda il nome della tabella derivata di destinazione; un presupposto comune tra gli sviluppatori T-SQL è che si tratti semplicemente di un nome o alias che si assegna alla tabella di destinazione. Allo stesso modo, considera la seguente query:

SELECT custid, companyname
FROM Sales.Customers AS C
WHERE country = N'USA';

Anche qui, il presupposto comune è che AS C sia solo un modo per rinominare, o alias, la tabella Clienti ai fini di questa query, a partire dalla fase di elaborazione della query logica in cui viene assegnato il nome e avanti. Tuttavia, dal punto di vista della teoria relazionale, c'è un significato più profondo in ciò che C rappresenta. C è ciò che è noto come una variabile di intervallo. C è una variabile di relazione derivata che si estende sulle tuple nella variabile di relazione di input Clienti. Nell'esempio precedente, C si estende sulle tuple in Clienti e valuta il paese del predicato =N'USA'. Le tuple per le quali il predicato restituisce true diventano parte della relazione di risultato C.

Un'espressione di tabella è una tabella

Con il background che ho fornito finora, quello che sto per spiegare dopo non dovrebbe sorprendere. La parte

di una definizione di tabella derivata è una tabella . Questo è il caso anche se è espresso come una query. Ricordi la proprietà di chiusura dell'algebra relazionale? Lo stesso vale per il resto delle suddette espressioni di tabella con nome (CTE, viste e TVF inline). Come hai già appreso, la tabella di SQL è la controparte della relazione della teoria relazionale , anche se non una controparte perfetta. Pertanto, un'espressione di tabella deve soddisfare determinati requisiti per garantire che il risultato sia una tabella, quelli che una query che non viene utilizzata come espressione di tabella non deve necessariamente farlo. Ecco tre requisiti specifici:

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

Analizziamo questi requisiti uno per uno, discutendo la rilevanza sia per la teoria relazionale che per SQL.

Tutte le colonne devono avere nomi

Ricorda che una relazione ha un titolo e un corpo. L'intestazione di una relazione è un insieme di attributi (colonne in SQL). Un attributo ha un nome e un nome di tipo ed è identificato dal suo nome. Una query che non viene utilizzata come espressione di tabella non deve necessariamente assegnare nomi a tutte le colonne di destinazione. Considera la seguente query come esempio:

SELECT empid, firstname, lastname,
  CONCAT_WS(N'/', country, region, city)
FROM HR.Employees;

Questa query genera il seguente output:

empid  firstname  lastname   (No column name)
------ ---------- ---------- -----------------
1      Sara       Davis      USA/WA/Seattle
2      Don        Funk       USA/WA/Tacoma
3      Judy       Lew        USA/WA/Kirkland
4      Yael       Peled      USA/WA/Redmond
5      Sven       Mortensen  UK/London
6      Paul       Suurs      UK/London
7      Russell    King       UK/London
8      Maria      Cameron    USA/WA/Seattle
9      Patricia   Doyle      UK/London

L'output della query ha una colonna anonima risultante dalla concatenazione degli attributi di posizione utilizzando la funzione CONCAT_WS. (A proposito, questa funzione è stata aggiunta in SQL Server 2017, quindi se stai eseguendo il codice in una versione precedente, sentiti libero di sostituire questo calcolo con un calcolo alternativo a tua scelta.) Questa query, quindi, non lo fa restituire una tabella, per non parlare di una relazione. Pertanto, non è valido utilizzare tale query come espressione di tabella/parte interna della query di una definizione di tabella derivata.

Provalo:

SELECT *
FROM ( SELECT empid, firstname, lastname,
         CONCAT_WS(N'/', country, region, city)
       FROM HR.Employees ) AS D;

Viene visualizzato il seguente errore:

Msg 8155, livello 16, stato 2, riga 50
Non è stato specificato alcun nome di colonna per la colonna 4 di 'D'.

Per inciso, noti qualcosa di interessante sul messaggio di errore? Si lamenta della colonna 4, evidenziando la differenza tra le colonne in SQL e gli attributi nella teoria relazionale.

La soluzione è, ovviamente, assicurarsi di assegnare esplicitamente i nomi alle colonne risultanti dai calcoli. T-SQL supporta parecchie tecniche di denominazione delle colonne. Ne citerò due.

È possibile utilizzare una tecnica di denominazione inline in cui si assegna il nome della colonna di destinazione dopo il calcolo e una clausola AS opzionale, come in < expression > [ AS ] < column name > , in questo modo:

SELECT empid, firstname, lastname, custlocation
FROM ( SELECT empid, firstname, lastname,
         CONCAT_WS(N'/', country, region, city) AS custlocation
       FROM HR.Employees ) AS D;

Questa query genera il seguente output:

empid  firstname  lastname   custlocation
------ ---------- ---------- ----------------
1      Sara       Davis      USA/WA/Seattle
2      Don        Funk       USA/WA/Tacoma
3      Judy       Lew        USA/WA/Kirkland
4      Yael       Peled      USA/WA/Redmond
5      Sven       Mortensen  UK/London
6      Paul       Suurs      UK/London
7      Russell    King       UK/London
8      Maria      Cameron    USA/WA/Seattle
9      Patricia   Doyle      UK/London

Usando questa tecnica, è molto facile quando si esamina il codice per dire quale nome di colonna di destinazione è assegnato a quale espressione. Inoltre, devi solo nominare le colonne che non hanno già nomi altrimenti.

Puoi anche utilizzare una tecnica di denominazione delle colonne più esterna in cui specifichi i nomi delle colonne di destinazione tra parentesi subito dopo il nome della tabella derivata, in questo modo:

SELECT empid, firstname, lastname, custlocation
FROM ( SELECT empid, firstname, lastname,
         CONCAT_WS(N'/', country, region, city)
       FROM HR.Employees ) AS D(empid, firstname, lastname, custlocation);

Con questa tecnica, tuttavia, devi elencare i nomi per tutte le colonne, comprese quelle che hanno già nomi. L'assegnazione dei nomi delle colonne di destinazione avviene per posizione, da sinistra a destra, ovvero il nome della prima colonna di destinazione rappresenta la prima espressione nell'elenco SELECT della query interna; il nome della seconda colonna di destinazione rappresenta la seconda espressione; e così via.

Si noti che in caso di incoerenza tra i nomi delle colonne interne ed esterne, ad esempio a causa di un bug nel codice, l'ambito dei nomi interni è la query interna o, più precisamente, la variabile di intervallo interna (qui implicitamente HR.Employees AS Employees) e l'ambito dei nomi esterni è la variabile di intervallo esterno (D nel nostro caso). C'è un po' più di coinvolgimento nell'ambito dei nomi di colonna che ha a che fare con l'elaborazione logica delle query, ma questo è un elemento per discussioni successive.

Il potenziale di bug con la sintassi di denominazione esterna è meglio spiegato con un esempio.

Esaminare l'output della query precedente, con il set completo di dipendenti dalla tabella HR.Employees. Quindi, considera la seguente query e, prima di eseguirla, prova a capire quali dipendenti ti aspetti di vedere nel risultato:

SELECT empid, firstname, lastname, custlocation
FROM ( SELECT empid, firstname, lastname,
         CONCAT_WS(N'/', country, region, city)
       FROM HR.Employees
       WHERE lastname LIKE N'D%' ) AS D(empid, lastname, firstname, custlocation)
WHERE firstname LIKE N'D%';

Se prevedi che la query restituisca un set vuoto per i dati di esempio forniti, poiché attualmente non ci sono dipendenti con un cognome e un nome che iniziano con la lettera D, il bug nel codice manca.

Ora esegui la query ed esamina l'output effettivo:

empid  firstname  lastname  custlocation
------ ---------- --------- ---------------
1      Davis      Sara      USA/WA/Seattle
9      Doyle      Patricia  UK/London

Cosa è successo?

La query interna specifica il nome come seconda colonna e il cognome come terza colonna nell'elenco SELECT. Il codice che assegna i nomi delle colonne di destinazione della tabella derivata nella query esterna specifica il cognome secondo e il nome terzo. I nomi in codice nome come cognome e cognome come nome nella variabile di intervallo D. In effetti, stai solo filtrando i dipendenti il ​​cui cognome inizia con la lettera D. Non stai filtrando i dipendenti sia con un cognome che con un nome che iniziano con la lettera D.

La sintassi dell'alias in linea non è soggetta a tali bug. Per uno, normalmente non alias una colonna che ha già un nome con cui sei soddisfatto. In secondo luogo, anche se vuoi assegnare un alias diverso a una colonna che ha già un nome, non è molto probabile che con la sintassi AS assegni l'alias sbagliato. Pensaci; quanto è probabile che scrivi in ​​questo modo:

SELECT empid, firstname, lastname, custlocation
FROM ( SELECT empid AS empid, firstname AS lastname, lastname AS firstname,
         CONCAT_WS(N'/', country, region, city) AS custlocation
       FROM HR.Employees
       WHERE lastname LIKE N'D%' ) AS D
WHERE firstname LIKE N'D%';

Ovviamente, non molto probabile.

Tutti i nomi delle colonne devono essere univoci

Tornando al fatto che l'intestazione di una relazione è un insieme di attributi, e dato che un attributo è identificato dal nome, i nomi degli attributi devono essere univoci per la stessa relazione. In una determinata query, puoi sempre fare riferimento a un attributo utilizzando un nome in due parti con il nome della variabile di intervallo come qualificatore, come in .. Quando il nome della colonna senza il qualificatore non è ambiguo, puoi omettere il prefisso del nome della variabile di intervallo. Ciò che è importante ricordare è ciò che ho detto prima sull'ambito dei nomi delle colonne. Nel codice che coinvolge un'espressione di tabella denominata, sia con una query interna (l'espressione di tabella) che con una query esterna, l'ambito dei nomi di colonna nella query interna sono le variabili di intervallo interne e l'ambito dei nomi di colonna nella query esterna query sono le variabili dell'intervallo esterno. Se la query interna coinvolge più tabelle di origine con lo stesso nome di colonna, puoi comunque fare riferimento a tali colonne in modo non ambiguo aggiungendo il nome della variabile di intervallo come prefisso. Se non si assegna un nome di variabile di intervallo in modo esplicito, ne viene assegnato uno implicitamente, come se si utilizzasse AS .

Considera la seguente query autonoma come esempio:

SELECT C.custid, O.custid, O.orderid
FROM Sales.Customers AS C
  LEFT OUTER JOIN Sales.Orders AS O
    ON C.custid = O.custid;

Questa query non fallisce con un errore di nome di colonna duplicato poiché una colonna custid è effettivamente denominata C.custid e l'altra O.custid nell'ambito della query corrente. Questa query genera il seguente output:

custid      custid      orderid
----------- ----------- -----------
1           1           10643
1           1           10692
1           1           10702
1           1           10835
1           1           10952
1           1           11011
2           2           10308
2           2           10625
2           2           10759
2           2           10926
...

Tuttavia, prova a utilizzare questa query come espressione di tabella nella definizione di una tabella derivata denominata CO, in questo modo:

SELECT *
FROM ( SELECT C.custid, O.custid, O.orderid
       FROM Sales.Customers AS C
         LEFT OUTER JOIN Sales.Orders AS O
           ON C.custid = O.custid ) AS CO;

Per quanto riguarda la query esterna, hai una variabile di intervallo denominata CO e l'ambito di tutti i nomi di colonna nella query esterna è quella variabile di intervallo. I nomi di tutte le colonne in una determinata variabile di intervallo (ricorda, una variabile di intervallo è una variabile di relazione) devono essere univoci. Quindi, ottieni il seguente errore:

Msg 8156, livello 16, stato 1, riga 80
La colonna "custid" è stata specificata più volte per "CO".

La correzione è ovviamente quella di assegnare nomi di colonna diversi alle due colonne custid per quanto riguarda la variabile di intervallo CO, in questo modo:

SELECT *
FROM ( SELECT C.custid AS custcustid, O.custid AS ordercustid, O.orderid
       FROM Sales.Customers AS C
         LEFT OUTER JOIN Sales.Orders AS O
           ON C.custid = O.custid ) AS CO;

Questa query genera il seguente output:

custcustid  ordercustid orderid
----------- ----------- -----------
1           1           10643
1           1           10692
1           1           10702
1           1           10835
1           1           10952
1           1           11011
2           2           10308
2           2           10625
2           2           10759
2           2           10926
...

Se segui le buone pratiche, elenchi in modo esplicito i nomi delle colonne nell'elenco SELECT della query più esterna. Poiché è coinvolta solo una variabile di intervallo, non è necessario utilizzare il nome in due parti per i riferimenti alle colonne esterne. Se desideri utilizzare il nome in due parti, anteponi ai nomi delle colonne il nome della variabile dell'intervallo esterno CO, in questo modo:

SELECT CO.custcustid, CO.ordercustid, CO.orderid
FROM ( SELECT C.custid AS custcustid, O.custid AS ordercustid, O.orderid
       FROM Sales.Customers AS C
         LEFT OUTER JOIN Sales.Orders AS O
           ON C.custid = O.custid ) AS CO;

Nessun ordine

C'è molto da dire sulle espressioni e sull'ordinamento delle tabelle con nome, abbastanza per un articolo a sé stante, quindi dedicherò un articolo futuro a questo argomento. Tuttavia, volevo toccare brevemente l'argomento qui poiché è così importante. Ricordiamo che il corpo di una relazione è un insieme di tuple e, analogamente, il corpo di una tabella è un insieme di righe. Un set non ha ordine. Tuttavia, SQL consente alla query più esterna di avere una clausola ORDER BY che serve un significato di ordinamento della presentazione, come dimostra la query seguente:

SELECT orderid, val
FROM Sales.OrderValues
ORDER BY val DESC;

Quello che devi capire, però, è che questa query non restituisce una relazione come risultato. Anche dal punto di vista di SQL, la query non restituisce una tabella come risultato, e quindi non lo è considerata un'espressione tabellare. Di conseguenza, non è valido utilizzare una query di questo tipo come espressione di tabella parte di una definizione di tabella derivata.

Prova a eseguire il seguente codice:

SELECT orderid, val
FROM ( SELECT orderid, val
       FROM Sales.OrderValues
       ORDER BY val DESC ) AS D;

Viene visualizzato il seguente errore:

Msg 1033, livello 15, stato 1, riga 124
La clausola ORDER BY non è valida nelle viste, nelle funzioni inline, nelle tabelle derivate, nelle sottoquery e nelle espressioni di tabelle comuni, a meno che non sia specificato anche TOP, OFFSET o FOR XML.

Mi occuperò dei a meno che parte del messaggio di errore a breve.

Se vuoi che la query più esterna restituisca un risultato ordinato, devi specificare la clausola ORDER BY nella query più esterna, in questo modo:

SELECT orderid, val
FROM ( SELECT orderid, val
       FROM Sales.OrderValues ) AS D
ORDER BY val DESC;

Per quanto riguarda i a meno che parte del messaggio di errore; T-SQL supporta il filtro TOP proprietario e il filtro OFFSET-FETCH standard. Entrambi i filtri si basano su una clausola ORDER BY nello stesso ambito di query per definire quali righe superiori filtrare. Questo è sfortunatamente il risultato di una trappola nella progettazione di queste funzionalità, che non separa l'ordinamento della presentazione dall'ordinamento del filtro. Comunque sia, sia Microsoft con il suo filtro TOP, sia lo standard con il suo filtro OFFSET-FETCH, consentono di specificare una clausola ORDER BY nella query interna purché specifichi anche il filtro TOP o OFFSET-FETCH, rispettivamente. Quindi, questa query è valida, ad esempio:

SELECT orderid, val
FROM ( SELECT TOP (3) orderid, val
       FROM Sales.OrderValues
       ORDER BY val DESC ) AS D;

Quando ho eseguito questa query sul mio sistema, ha generato il seguente output:

orderid  val
-------- ---------
10865    16387.50
10981    15810.00
11030    12615.05

Ciò che è importante sottolineare, tuttavia, è che l'unico motivo per cui la clausola ORDER BY è consentita nella query interna è supportare il filtro TOP. Questa è l'unica garanzia che ottieni per quanto riguarda l'ordine. Poiché anche la query esterna non ha una clausola ORDER BY, non ottieni alcuna garanzia per alcun ordinamento di presentazione specifico da questa query, nonostante qualunque sia il comportamento osservato. Questo è sia il caso in T-SQL che nello standard. Ecco una citazione dallo standard che affronta questa parte:

"L'ordinamento delle righe della tabella specificate dalla è garantito solo per la che contiene immediatamente la ."

Come accennato, c'è molto altro da dire sulle espressioni delle tabelle e sull'ordinamento, cosa che farò in un prossimo articolo. Fornirò anche esempi che dimostrano come la mancanza della clausola ORDER BY nella query esterna significhi che non ottieni alcuna garanzia di ordinazione della presentazione.

Quindi, un'espressione di tabella, ad esempio una query interna in una definizione di tabella derivata, è una tabella. Allo stesso modo, anche una tabella derivata (in senso specifico) è una tabella. Non è un tavolo basso, ma è comunque un tavolo. Lo stesso vale per CTE, visualizzazioni e TVF in linea. Non sono tabelle di base, ma derivate (in senso più generale), ma sono comunque tabelle.

Difetti di progettazione

Le tabelle derivate presentano due principali carenze nella loro progettazione. Entrambi hanno a che fare con il fatto che la tabella derivata è definita nella clausola FROM della query esterna.

Un difetto di progettazione ha a che fare con il fatto che se è necessario eseguire una query su una tabella derivata da una query esterna e, a sua volta, utilizzare tale query come espressione di tabella in un'altra definizione di tabella derivata, si finisce per annidare quelle query di tabella derivata. In informatica, l'annidamento esplicito del codice che coinvolge più livelli di annidamento tende a produrre codice complesso che è difficile da mantenere.

Ecco un esempio molto semplice che lo dimostra:

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;

Questo codice restituisce gli anni dell'ordine e il numero di clienti che hanno effettuato ordini durante ogni anno, solo per gli anni in cui il numero di clienti che hanno effettuato ordini era maggiore di 70.

La motivazione principale per l'utilizzo delle espressioni di tabella qui è quella di poter fare riferimento più volte a un alias di colonna. La query più interna utilizzata come espressione di tabella per la tabella derivata D1 interroga la tabella Sales.Orders e assegna il nome della colonna orderyear all'espressione YEAR(orderdate) e restituisce anche la colonna custid. La query su D1 raggruppa le righe da D1 per orderyear e restituisce orderyear nonché il numero distinto di clienti che hanno effettuato ordini durante l'anno in questione alias numcusts. Il codice definisce una tabella derivata denominata D2 basata su questa query. La query più esterna rispetto alle query D2 e ​​filtra solo gli anni in cui il numero di clienti che hanno effettuato ordini era maggiore di 70.

Un tentativo di rivedere questo codice o risolverlo in caso di problemi è complicato a causa dei molteplici livelli di annidamento. Invece di rivedere il codice nel modo più naturale dall'alto verso il basso, ti ritrovi a doverlo analizzare partendo dall'unità più interna e andando gradualmente verso l'esterno, poiché è più pratico.

Lo scopo principale dell'utilizzo di tabelle derivate in questo esempio era semplificare il codice evitando la necessità di ripetere le espressioni. Ma non sono sicuro che questa soluzione raggiunga questo obiettivo. In questo caso, probabilmente faresti meglio a ripetere alcune espressioni, evitando la necessità di utilizzare del tutto tabelle derivate, in questo modo:

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

Tieni presente che sto mostrando un esempio molto semplice qui a scopo illustrativo. Immagina un codice di produzione con più livelli di annidamento e con un codice più lungo ed elaborato e puoi vedere come diventa sostanzialmente più complicato da mantenere.

Un altro difetto nella progettazione delle tabelle derivate ha a che fare con i casi in cui è necessario interagire con più istanze della stessa tabella derivata. Considera la seguente query come esempio:

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;

Questo codice calcola il numero di ordini elaborati in ogni anno, nonché la differenza rispetto all'anno precedente. Ignora il fatto che ci sono modi più semplici per ottenere la stessa attività con le funzioni della finestra:sto usando questo codice per illustrare un certo punto, quindi l'attività stessa e i diversi modi per risolverla non sono significativi.

Un join è un operatore di tabella che tratta i suoi due input come un insieme, il che significa che non c'è ordine tra di loro. Sono indicati come input sinistro e destro in modo da poter contrassegnare uno di essi (o entrambi) come tabella conservata in un join esterno, ma comunque non esiste un primo e un secondo tra loro. È consentito utilizzare tabelle derivate come input di join, ma il nome della variabile di intervallo assegnato all'input sinistro non è accessibile nella definizione dell'input destro. Questo perché entrambi sono concettualmente definiti nello stesso passaggio logico, come se fossero nello stesso momento. Di conseguenza, quando si uniscono tabelle derivate, non è possibile definire due variabili di intervallo basate su un'espressione di tabella. Sfortunatamente, devi ripetere il codice, definendo due variabili di intervallo basate su due copie identiche del codice. Questo ovviamente complica la manutenibilità del codice e aumenta la probabilità di bug. Ogni modifica apportata a un'espressione di tabella deve essere applicata anche all'altra.

Come spiegherò in un prossimo articolo, i CTE, nella loro progettazione, non incorrono in questi due difetti in cui incorrono le tabelle derivate.

Costruttore di valori di tabella

Un costruttore di valori di tabella consente di costruire un valore di tabella basato su espressioni scalari autonome. È quindi possibile utilizzare tale tabella in una query esterna proprio come si utilizza una tabella derivata basata su una query interna. In un prossimo articolo parlerò delle tabelle derivate laterali and correlations in detail, and I’ll show more sophisticated forms of table value constructors. In this article, though, I’ll focus on a simple form that is based purely on self-contained scalar expressions.

The general syntax for a query against a table value constructor is as follows:

SELECT
) AS
(
);

The table value constructor is defined in the FROM clause of the outer query.

The table’s body is made of a VALUES clause, followed by a comma separated list of pairs of parentheses, each defining a row with a comma separated list of expressions forming the row’s values.

The table’s heading is a comma separated list of the target column names. I’ll talk about a shortcoming of this syntax regarding the table’s heading shortly.

The following code uses a table value constructor to define a table called MyCusts with three columns called custid, companyname and contractdate, and three rows:

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

The above code is equivalent (both logically and in performance terms) in T-SQL to the following alternative:

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

The two are internally algebrized the same way. The syntax with the VALUES clause is standard whereas the syntax with the unified FROMless queries isn’t, hence I prefer the former.

There is a shortcoming in the design of table value constructors in both standard SQL and in T-SQL. Remember that the heading of a relation is made of a set of attributes, and an attribute has a name and a type name. In the table value constructor’s syntax, you specify the column names, but not their data types. Suppose that you need the custid column to be of a SMALLINT type, the companyname column of a VARCHAR(50) type, and the contractdate column of a DATE type. It would have been good if we were able to define the column types as part of the definition of the table’s heading, like so (this syntax isn’t supported):

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

That’s of course just wishful thinking.

The way it works in T-SQL, is that each literal that is based on a constant has a predetermined type irrespective of context. For instance, can you guess what the types of the following literals are:

  • 1
  • 2147483647
  • 2147483648
  • 1E
  • '1E'
  • '20200212'

Is 1 considered BIT, INT, SMALLINT, other?

Is 1E considered VARBINARY(1), VARCHAR(2), other?

Is '20200212' considered DATE, DATETIME, VARCHAR(8), CHAR(8), other?

There’s a simple trick to figure out the default type of a literal, using the SQL_VARIANT_PROPERTY function with the 'BaseType' property, like so:

SELECT SQL_VARIANT_PROPERTY(2147483648, 'BaseType');

What happens is that SQL Server implicitly converts the literal to SQL_VARIANT—since that’s what the function expects—but preserves its base type. It then reports the base type as requested.

Similarly, you can query other properties of the input value, like the maximum length (MaxLength), Precision, Scale, and so on.

Try it with the aforementioned literal values, and you will get the following:

  • 1:INT
  • 2147483647:INT
  • 2147483648:NUMERIC(10, 0)
  • 1E:FLOAT
  • '1E':VARCHAR(2)
  • '20200212':VARCHAR(8)

As you can see, SQL Server has default assumptions about the data type, maximum length, precision, scale, and so on.

There are some cases where you need to specify a literal of a certain type, but you cannot do it directly in T-SQL. For example, you cannot specify a literal of the following types directly:BIT, TINYINT, BIGINT, all date and time types, and quite a few others. Unfortunately, T-SQL doesn’t provide a selector property for its types, which would have served exactly the needed purpose of selecting a value of the given type. Of course, you can always convert an expression’s type explicitly using the CAST or CONVERT function, as in CAST(5 AS SMALLINT). If you don’t, SQL Server will sometimes need to implicitly convert some of your expressions to a different type based on its implicit conversion rules. For example, when you try to compare values of different types, e.g., WHERE datecol ='20200212', assuming datecol is of a DATE type. Another example is when you specify a literal in an INSERT or an UPDATE statement, and the literal’s type is different than the target column’s type.

If all this is not confusing enough, set operators like UNION ALL rely on data type precedence to define the target column types—and remember, a table value constructor is algebrized like a series of UNION ALL operations. Consider the table value constructor shown earlier:

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

Each literal here has a predetermined type. 2, 3 and 5 are all of an INT type, so clearly the custid target column type is INT. If you had the values 1000000000, 3000000000 and 2000000000, the first and the third are considered INT and the second is considered NUMERIC(10, 0). According to data type precedence NUMERIC (same as DECIMAL) is stronger than INT, hence in such a case the target column type would be NUMERIC(10, 0).

If you want to figure out which data types SQL Server chooses for the target columns in your table value constructor, you have a few options. One is to use a SELECT INTO statement to write the table value constructor’s data into a temporary table, and then query the metadata for the temporary table, like so:

SELECT custid, companyname, contractdate
INTO #MyCusts
FROM ( VALUES( 2, 'Cust 2', '20200212' ),
             ( 3, 'Cust 3', '20200118' ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate);
 
SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlength
FROM tempdb.sys.columns
WHERE OBJECT_ID = OBJECT_ID(N'tempdb..#MyCusts');

Here’s the output of this code:

colname       typename   maxlength
------------- ---------- ---------
custid        int        4
companyname   varchar    6
contractdate  varchar    8

You can then drop the temporary table for cleanup:

DROP TABLE IF EXISTS #MyCusts;

Another option is to use the SQL_VARIANT_PROPERTY, which I mentioned earlier, like so:

SELECT TOP (1)
  SQL_VARIANT_PROPERTY(custid, 'BaseType')        AS custid_typename,
  SQL_VARIANT_PROPERTY(custid, 'MaxLength')       AS custid_maxlength,
  SQL_VARIANT_PROPERTY(companyname, 'BaseType')   AS companyname_typename,
  SQL_VARIANT_PROPERTY(companyname, 'MaxLength')  AS companyname_maxlength,
  SQL_VARIANT_PROPERTY(contractdate, 'BaseType')  AS contractdate_typename,
  SQL_VARIANT_PROPERTY(contractdate, 'MaxLength') AS contractdate_maxlength
FROM ( VALUES( 2, 'Cust 2', '20200212' ),
             ( 3, 'Cust 3', '20200118' ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate);

This code generates the following output (formatted for readability):

custid_typename       custid_maxlength
--------------------  ---------------- 
int                   4                

companyname_typename  companyname_maxlength 
--------------------  --------------------- 
varchar               6                     

contractdate_typename contractdate_maxlength
--------------------- ----------------------
varchar               8

So, what if you need to control the types of the target columns? As mentioned earlier, say you need custid to be SMALLINT, companyname VARCHAR(50), and contractdate DATE.

Don’t be misled to think that it’s enough to explicitly convert just one row’s values. If a corresponding value’s type in any other row is considered stronger, it would dictate the target column’s type. Here’s an example demonstrating this:

SELECT custid, companyname, contractdate
INTO #MyCusts1
FROM ( VALUES( CAST(2 AS SMALLINT), CAST('Cust 2' AS VARCHAR(50)), CAST('20200212' AS DATE)),
             ( 3, 'Cust 3', '20200118' ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate);
 
SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlength
FROM tempdb.sys.columns
WHERE OBJECT_ID = OBJECT_ID(N'tempdb..#MyCusts1');

Questo codice genera il seguente output:

colname       typename  maxlength
------------- --------- ---------
custid        int       4
companyname   varchar   50
contractdate  date      3

Notice that the type for custid is INT.

The same applies never mind which row’s values you explicitly convert, if you don’t convert all of them. For example, here the code explicitly converts the types of the values in the second row:

SELECT custid, companyname, contractdate
INTO #MyCusts2
FROM ( VALUES( 2, 'Cust 2', '20200212'),
             ( CAST(3 AS SMALLINT), CAST('Cust 3' AS VARCHAR(50)), CAST('20200118' AS DATE) ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate);
 
SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlength
FROM tempdb.sys.columns
WHERE OBJECT_ID = OBJECT_ID(N'tempdb..#MyCusts2');

Questo codice genera il seguente output:

colname       typename  maxlength
------------- --------- ---------
custid        int       4
companyname   varchar   50
contractdate  date      3

As you can see, custid is still of an INT type.

You basically have two main options. One is to explicitly convert all values, like so:

SELECT custid, companyname, contractdate
INTO #MyCusts3
FROM ( VALUES( CAST(2 AS SMALLINT), CAST('Cust 2' AS VARCHAR(50)), CAST('20200212' AS DATE)),
             ( CAST(3 AS SMALLINT), CAST('Cust 3' AS VARCHAR(50)), CAST('20200118' AS DATE)),
             ( CAST(5 AS SMALLINT), CAST('Cust 5' AS VARCHAR(50)), CAST('20200401' AS DATE)) )
       AS MyCusts(custid, companyname, contractdate);
 
SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlength
FROM tempdb.sys.columns
WHERE OBJECT_ID = OBJECT_ID(N'tempdb..#MyCusts3');

This code generates the following output, showing all target columns have the desired types:

colname       typename  maxlength
------------- --------- ---------
custid        smallint  2
companyname   varchar   50
contractdate  date      3

That’s a lot of coding, though. Another option is to apply the conversions in the SELECT list of the query against the table value constructor, and then define a derived table against the query that applies the conversions, like so:

SELECT custid, companyname, contractdate
INTO #MyCusts4
FROM ( SELECT
         CAST(custid AS SMALLINT) AS custid,
         CAST(companyname AS VARCHAR(50)) AS companyname,
         CAST(contractdate AS DATE) AS contractdate
       FROM ( VALUES( 2, 'Cust 2', '20200212' ),
                    ( 3, 'Cust 3', '20200118' ),
                    ( 5, 'Cust 5', '20200401' ) )
              AS D(custid, companyname, contractdate) ) AS MyCusts;
 
SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlength
FROM tempdb.sys.columns
WHERE OBJECT_ID = OBJECT_ID(N'tempdb..#MyCusts4');

Questo codice genera il seguente output:

colname       typename  maxlength
------------- --------- ---------
custid        smallint  2
companyname   varchar   50
contractdate  date      3

The reasoning for using the additional derived table is due to how logical query processing is designed. The SELECT clause is evaluated after FROM, WHERE, GROUP BY and HAVING. By applying the conversions in the SELECT list of the inner query, you allow expressions in all clauses of the outermost query to interact with the columns with the proper types.

Back to our wishful thinking, clearly, it would be good if we ever get a syntax that allows explicit control of the types in the definition of the table value constructor’s heading, like so:

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

When you’re done, run the following code for cleanup:

DROP TABLE IF EXISTS #MyCusts1, #MyCusts2, #MyCusts3, #MyCusts4;

Used in modification statements

T-SQL allows you to modify data through table expressions. That’s true for derived tables, CTEs, views and inline TVFs. What gets modified in practice is some underlying base table that is used by the table expression. I have much to say about modifying data through table expressions, and I will in a future article dedicated to this topic. Here, I just wanted to briefly mention the types of modification statements that specifically support derived tables, and provide the syntax.

Derived tables can be used as the target table in DELETE and UPDATE statements, and also as the source table in the MERGE statement (in the USING clause). They cannot be used in the TRUNCATE statement, and as the target in the INSERT and MERGE statements.

For the DELETE and UPDATE statements, the syntax for defining the derived table is a bit awkward. You don’t define the derived table in the DELETE and UPDATE clauses, like you would expect, but rather in a separate FROM clause. You then specify the derived table name in the DELETE or UPDATE clause.

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

DELETE [ FROM ]

FROM (
) [ AS ]
[ () ]
[ WHERE ];

As an example (don’t actually run it), the following code deletes all US customers with a customer ID that is greater than the minimum for the same region (the region column represents the state for US customers):

DELETE FROM UC
FROM ( SELECT *, ROW_NUMBER() OVER(PARTITION BY region ORDER BY custid) AS rownum
       FROM Sales.Customers
       WHERE country = N'USA' ) AS UC
WHERE rownum > 1;

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

UPDATE

SET
FROM (
) [ AS ]
[ () ]
[ WHERE ];

As you can see, from the perspective of the definition of the derived table, it’s quite similar to the syntax of the DELETE statement.

As an example, the following code changes the company names of US customers to one using the format N'USA Cust ' + rownum, where rownum represents a position based on customer ID ordering:

BEGIN TRAN;
 
UPDATE UC
  SET companyname = newcompanyname
    OUTPUT
      inserted.custid,
      deleted.companyname AS oldcompanyname,
      inserted.companyname AS newcompanyname
FROM ( SELECT custid, companyname,
         N'USA Cust ' + CAST(ROW_NUMBER() OVER(ORDER BY custid) AS NVARCHAR(10)) AS newcompanyname 
       FROM Sales.Customers
       WHERE country = N'USA' ) AS UC;
 
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 company names:

custid  oldcompanyname  newcompanyname
------- --------------- ----------------
32      Customer YSIQX  USA Cust 1
36      Customer LVJSO  USA Cust 2
43      Customer UISOJ  USA Cust 3
45      Customer QXPPT  USA Cust 4
48      Customer DVFMB  USA Cust 5
55      Customer KZQZT  USA Cust 6
65      Customer NYUHS  USA Cust 7
71      Customer LCOUJ  USA Cust 8
75      Customer XOJYP  USA Cust 9
77      Customer LCYBZ  USA Cust 10
78      Customer NLTYP  USA Cust 11
82      Customer EYHKM  USA Cust 12
89      Customer YBQTI  USA Cust 13

That’s it for now on the topic.

Riepilogo

Derived tables are one of the four main types of named table expressions that T-SQL supports. In this article I focused on the logical aspects of derived tables. I described the syntax for defining them and their scope.

Remember that a table expression is a table and as such, all of its columns must have names, all column names must be unique, and the table has no order.

The design of derived tables incurs two main flaws. In order to query one derived table from another, you need to nest your code, causing it to be more complex to maintain and troubleshoot. If you need to interact with multiple occurrences of the same table expression, using derived tables you are forced to duplicate your code, which hurts the maintainability of your solution.

You can use a table value constructor to define a table based on self-contained expressions as opposed to querying some existing base tables.

You can use derived tables in modification statements like DELETE and UPDATE, though the syntax for doing so is a bit awkward.