PIVOT e UNPIVOT per principianti

Prima di cominciare

 
Ovviamente, se siete arrivati ad interessarvi di PIVOT, tanto principianti non lo siete. Non è mia intenzione offendere nessuno.
Nell'SQL "moderno" è presente un comando che, in certi contesti, può risolvere problematiche legate alla differenza esistente fra come i dati sono strutturati all'interno di un database e la presentazione che ne dev'essere fatta all'utente.
Il contesto e l'utilizzo di PIVOT / UNPIVOT, per risolvere le problematiche che ho accennato, sono di più facile comprensione tramite un esempio, che racchiude un caso "tipico".
Nota: Negli esempi di codice ho usato la sintassi T-SQL, propria di SqlServer; nelle altre versioni di SQL non penso ci siano grosse differenze.

Il fratello maggiore: PIVOT

 
Iniziamo col considerare una classica tabella contenente i dati di fatturazione per un'Azienda. In questo esempio, ovviamente, i dati sono ridotti al minimo indispensabile allo scopo di questo tutorial.
WITH
RecordsetOriginale AS (
SELECT Anno, Agente, ImportoFatturato
FROM TabFatturato
WHERE Anno IN (2015,2016)
)
SELECT * FROM RecordsetOriginale
Anno Agente ImportoFatturato
2015Agente AAA6320.58
2015Agente BBB5689.35
2015Agente CCC157896.25
2016Agente AAA8541.21
2016Agente CCC7563.26
2016Agente DDD10256.36
.........
I dati sono organizzati in modo classico: il fatturato è diviso per Agente e per Anno.
Ora, partendo da questa struttura, è possibile che la sezione Contabilità della suddetta Azienda, vi chieda di visualizzare questi dati in modo differente da come sono organizzati. La richiesta potrebbe facilmente essere: si possono mostrare i dati del Fatturato incolonnati per Anni di fatturazione e gli Agenti sulle righe, in modo da visualizzare l'andamento di Fatturato per Agente duranti gli Anni? In pratica si vorrebbe una visualizzazione dati come nella seguente tabella:
Agente 2015
(ImportoFatturato)
2016
(ImportoFatturato)
Agente AAA6320.588541.21
Agente BBB5689.35---
Agente CCC157896.257563.26
Agente DDD---10256.36
.........
Questo genere di richiesta può essere affrontata in maniera abbastanza semplice con dei comandi già presenti in SQL. L'alternativa sarebbe elaborare noi stessi, tramite codice, i dati prima di presentarli come voluto. Ovviamente l'alternativa è la strada sbagliata: perché reinventare l'acqua calda? I database relazionali moderni prevedono i comandi che fanno già questo genere di operazioni: PIVOT e UNPIVOT. Partiamo dal primo.
PIVOT permette di utilizzare i valori di un campo in modo che diventino colonne del dataset risultante e contemporaneamente traslare i valori di un altro campo in modo che diventino i valori delle nuove colonne (è più difficile spiegarlo a parole che vederlo in pratica). È quello che è accaduto nella tabella di esempio precedente. I valori del campo "Anno" (2015, 2016) sono diventati nuove colonne. I valori del campo "ImportoFatturato" sono diventati i valori delle nuove colonne. Il campo "Agente" invece rimane al suo posto, perché escluso dal PIVOT.
Vediamo il codice SQL che permette di ottenere questo:
WITH
RecordsetOriginale AS (
SELECT Anno, Agente, ImportoFatturato FROM TabFatturato
WHERE Anno IN (2015,2016)
),
DSBase_PIVOTATO AS (
SELECT * FROM RecordsetOriginale
PIVOT (MAX(ImportoFatturato) FOR Anno IN([2015],[2016])) NomeObbligatorioPerIlPivot
)
SELECT * FROM DSBase_PIVOTATO
Spiegazioni nel merito:
  • Con WITH si dichiarano due sub-query: la prima genera un dataset contenente i dati "grezzi" estrapolati dalla tabella "madre", la seconda applica il PIVOT ai dati della prima.
  • PIVOT necessita di una funzione aggregatrice per i dati contenuti nel campo i cui valori verranno traslati, "ImportoFatturato" nel nostro caso. La funzione MAX() mi permette di avere tale funzione aggregatrice senza stravolgere il valore del dato stesso.
  • FOR introduce il campo i cui valori diverranno nuove colonne, "Anno" nel nostro esempio.
  • Il successivo IN permette di specificare quali valori sono da utilizzare per creare le nuove colonne. È buona norma delimitare questi valori con la sintassi utilizzata per isolare i nomi degli oggetti, [ ] in T-SQL.
  • "NomeObbligatorioPerIlPivot" è necessario per rispettare la sintassi di PIVOT, ma nel nostro caso ha poca importanza.
  • Finito con le sub-query, utilizzo un semplice comando SQL per estrapolare i dati "pivotati".
Il recordset finale risultante sarà:
Agente 2015 2016
Agente AAA6320.588541.21
Agente BBB5689.35NULL
Agente CCC157896.257563.26
Agente DDDNULL10256.36
.........
Un'ultima annotazione. È interessante notare che PIVOT si prende la briga, durante l'operazione di traslazione, di verificare che esista un valore da traslare e, in caso contrario, di restituire NULL. Questo permette di avere in lista anche Agenti che non hanno fatturato per uno degli anni presi in considerazione. Nel nostro esempio mi riferisco all'Agente BBB o DDD.

Il fratello minore: UNPIVOT

 
Non è per denigrarlo, è solo che si usa di meno. UNPIVOT, per farla semplice, fa il contrario di PIVOT. In realtà non è proprio così, ma per questa guida possiamo semplificarla in questo modo: UNPIVOT prende i dati contenuti in più colonne e li raggruppa in una nuova colonna.
Come per PIVOT, anche per UNPIVOT è più facile capirne il funzionamento tramite un esempio. Partendo da quello precedente, utilizzato con PIVOT, approdiamo ad un nuovo scenario: come fare se i valori da traslare appartengono a più colonne?
WITH
RecordsetOriginale AS (
SELECT Anno, Agente, ImportoFatturato, ImportoMargine FROM TabFatturato
WHERE Anno IN (2015,2016)
)
SELECT * FROM RecordsetOriginale
Che produrrà questo recordset base:
Anno Agente ImportoFatturato ImportoMargine
2015Agente AAA6320.581325.66
2015Agente BBB5689.35986.24
2015Agente CCC157896.253844.82
2016Agente AAA8541.212745.20
2016Agente CCC7563.262056.33
2016Agente DDD10256.362657.25
............
Nella tabella precedente, oltre alla colonna "ImportoFatturato", è ora presente anche la colonna "ImportoMargine", che contiene il margine operativo realizzato dall'Agente. È solitamente un dato importante, quindi è ragionevole che vi venga richiesta una presentazione dei dati come nella tabella seguente:
Agente 2015
(ImportoFatturato)
2016
(ImportoFatturato)
2015
(ImportoMargine)
2016
(ImportoMargine)
Agente AAA6320.588541.211325.662745.20
Agente BBB5689.35---986.24---
Agente CCC157896.257563.263844.822056.33
Agente DDD---10256.36---2657.25
...............
Questo risultato non si può ottenere con un PIVOT semplice, in quanto la traslazione è possibile solo per un singolo campo. Per ovviare a questo impedimento, si usa una tecnica che consiste nel combinare i comandi UNPIVOT e PIVOT in maniera seriale: UNPIVOT ridurrà le colonne i cui valori sono da traslare ad una sola colonna, PIVOT traslerà i valori di quest'ultima per ottenere il risultato finale voluto.
Iniziamo con l'applicare l'UNPIVOT al dataset base:
WITH
RecordsetOriginale AS (
SELECT Anno, Agente, ImportoFatturato, ImportoMargine FROM TabFatturato
WHERE Anno IN (2015,2016)
),
DSBase_UNPIVOT AS (
SELECT * FROM (
SELECT Agente,
CASE
WHEN ColonnaAnalizzata = 'ImportoFatturato' THEN 'F'
WHEN ColonnaAnalizzata = 'ImportoMargine' THEN 'M'
END + CAST(Anno AS varchar(4)) NomeColonnaUNPIVOT,
value
FROM RecordsetOriginale
UNPIVOT (
value
FOR ColonnaAnalizzata IN ([ImportoFatturato],[ImportoMargine])
) NomeObbligatorioUnPivot
) UnPivotResulSet
)
Che produrrà il seguente poco appetibile risultato:
Agente NomeColonnaUNPIVOT value
Agente AAAF20156320.58
Agente AAAM20151325.66
Agente BBBF20155689.35
Agente BBBM2015986.24
Agente CCCF2015157896.25
Agente CCCM20153844.82
Agente AAAF20168541.21
Agente AAAM20162745.20
Agente CCCF20167563.26
Agente CCCM20162056.33
Agente DDDF201610256.36
Agente DDDM20162657.25
.........
Guardando la tabella risultato, è abbastanza evidente capire cosa ha fatto l'UNPIVOT:
  • Per ogni record del dataset base sono stati creati due record, uno per il Fatturato e l'altro per il Margine.
  • È stata creata una nuova colonna "NomeColonnaUNPIVOT", nella quale sono confluiti i nomi delle colonne specificate nel FOR...IN ("ImportoFatturato" e "ImportoMargine") opportunamente ridefiniti in base alla sorgente e all'Anno ("F2015", "M2015", etc.).
  • Il valore originale delle colonne specificate nel FOR...IN è stato mantenuto tramite l'utilizzo della keyword "value".
NOTA IMPORTANTE: I campi che confluiscono nella colonna "value" (nel nostro caso "ImportoFatturato" e "ImportoMargine") devono essere dello stesso tipo! Se non lo fossero, è necessario operare un opportuno casting dei dati prima di poter passare i dati all'UNPIVOT.
A questo punto possiamo applicare un PIVOT al dataset risultante dall'UNPIVOT, e traslare gli importi in nuove colonne che avranno il nome preso dai valori della colonna "NomeColonnaUNPIVOT" (ovvero "F2015", "M2015", etc.). Come già detto precedentemente, un esempio visivo chiarirà meglio il concetto:
WITH
RecordsetOriginale AS (
SELECT Anno, Agente, ImportoFatturato, ImportoMargine FROM TabFatturato
WHERE Anno IN (2015,2016)
),
DsBase_UNPIVOT_PIVOT AS (
SELECT * FROM (
SELECT Agente,
CASE
WHEN ColonnaAnalizzata = 'ImportoFatturato' THEN 'F'
WHEN ColonnaAnalizzata = 'ImportoMargine' THEN 'M'
END + CAST(Anno AS varchar(4)) NomeColonnaUNPIVOT,
value
FROM RecordsetOriginale
UNPIVOT (
value
FOR ColonnaAnalizzata IN ([ImportoFatturato],[ImportoMargine])
) NomeObbligatorioUnPivot
) UnPivotResulSet
PIVOT (MAX(value) FOR NomeColonnaUNPIVOT IN([F2015],[M2015],[F2016],[M2016])) NomeObbligatorioPivot
)
SELECT * FROM DsBase_UNPIVOT_PIVOT
Che produrrà un meraviglioso:
Agente F2015 M2015 F2016 M2016
Agente AAA6320.581325.668541.212745.20
Agente BBB5689.35986.24NULLNULL
Agente CCC157896.253844.827563.262056.33
Agente DDDNULLNULL10256.362657.25
...............
Che è la presentazione dei dati che volevamo.
Concludo con un'ultima precisazione e un'ultimissima raccomandazione: questo è un tutorial estremamente semplificato, con l'unico scopo di spiegare uno dei modi da cui trarre giovamento nell'utilizzare i comandi PIVOT ed UNPIVOT; se volete approfondire, e lo consiglio, fate riferimento alla documentazione del database che state utilizzando.

Esclusione di Responsabilità

 
Tutto il software scaricato da (e/o il codice fornito da) questo sito è fornito "COSÌ COM'È" senza alcuna garanzia, sia espressa che implicita. L'autore non potrà essere ritenuto responsabile per eventuali danni derivanti dall'utilizzo del software e/o del codice.
Per qualsiasi chiarimento e/o suggerimento, potete contattarmi all'indirizzo e-mail: info@gmas.it
Torna su
Testi, immagini, filmati, software sono opera mia, salvo dove diversamente specificato.