Quando un report mostra mesi, categorie o canali disposti uno sotto l’altro, la lettura può diventare scomoda. Il pattern sql server pivot risolve il problema trasformando i valori di una colonna in nuove colonne, così da ottenere una tabella pronta per dashboard, analisi e report aziendali. Qui mostro la sintassi, un esempio pratico, il pivot dinamico, le alternative con CASE e gli errori che incidono davvero sul risultato e sulle prestazioni.
Trasformare righe in colonne con controllo sui dati
- PIVOT converte i valori distinti di una colonna in colonne dell’output.
- Serve quasi sempre una funzione di aggregazione come SUM, COUNT o AVG.
- L’elenco dentro IN è statico, a meno di usare SQL dinamico.
- I valori mancanti producono NULL, che può essere sostituito con zero.
- Per pochi campi, l’aggregazione condizionale con CASE può essere più leggibile.
Che cosa fa davvero PIVOT in SQL Server
PIVOT ruota una tabella trasformando i valori unici di una colonna in più colonne. Se una tabella contiene Area, Mese e Importo, il mese può diventare l’intestazione delle colonne mentre l’importo viene sommato per area.
Il risultato non è soltanto un cambio estetico. Il pivot crea una nuova forma dei dati, più adatta alla consultazione umana e agli strumenti di business intelligence, ma spesso meno adatta alla memorizzazione. Per questo lo uso soprattutto nell’ultimo passaggio di una query o in una vista dedicata al reporting.
La documentazione ufficiale di Microsoft Learn chiarisce anche un punto importante: PIVOT esegue un’aggregazione sui valori che restano dopo aver identificato la colonna da ruotare. In pratica, se esistono più vendite per la stessa area e lo stesso mese, SQL Server deve sapere come combinarle.
Un esempio completo con mesi e aree
Immaginiamo una tabella Vendite con tre colonne essenziali. La query seguente è autonoma e usa una CTE per rendere l’esempio immediatamente eseguibile:
WITH Vendite AS
(
SELECT *
FROM (VALUES
('Nord', 'Gennaio', 12000),
('Nord', 'Febbraio', 13500),
('Nord', 'Marzo', 12800),
('Centro','Gennaio', 9800),
('Centro','Febbraio',10200),
('Sud', 'Gennaio', 7600),
('Sud', 'Marzo', 8400)
) AS Dati(Area, Mese, Importo)
)
SELECT
Area,
[Gennaio],
[Febbraio],
[Marzo]
FROM Vendite
PIVOT
(
SUM(Importo)
FOR Mese IN ([Gennaio], [Febbraio], [Marzo])
) AS P
ORDER BY Area;
La parte più importante è FOR Mese IN (...). Qui stabilisco quali valori diventeranno colonne. Le parentesi quadre sono necessarie quando il valore viene usato come identificatore, soprattutto se contiene spazi, caratteri speciali o coincide con una parola riservata.
La funzione SUM(Importo) somma le righe appartenenti alla stessa area e allo stesso mese. Se ci fossero due transazioni Nord a gennaio, il risultato mostrerebbe il loro totale, non due colonne o due righe separate.
Nel risultato, il mese di marzo dell’area Centro sarà NULL, perché non esiste una riga corrispondente. Se per il report è preferibile visualizzare zero, basta aggiungere un livello esterno:
SELECT
Area,
COALESCE([Gennaio], 0) AS [Gennaio],
COALESCE([Febbraio], 0) AS [Febbraio],
COALESCE([Marzo], 0) AS [Marzo]
FROM
(
SELECT Area, Mese, Importo
FROM dbo.Vendite
) AS S
PIVOT
(
SUM(Importo)
FOR Mese IN ([Gennaio], [Febbraio], [Marzo])
) AS P;Come preparare il dataset prima della rotazione
Molti problemi attribuiti a PIVOT nascono in realtà dai dati in ingresso. Prima di ruotare le righe, controllo sempre che la query sorgente contenga soltanto le colonne necessarie, filtri già applicati e valori coerenti nella colonna pivot.
Per esempio, se voglio analizzare soltanto il 2026, conviene filtrare prima della rotazione:
SELECT
Area,
[Gennaio],
[Febbraio],
[Marzo]
FROM
(
SELECT
Area,
DATENAME(MONTH, DataVendita) AS Mese,
Importo
FROM dbo.Vendite
WHERE DataVendita >= '20260101'
AND DataVendita < '20270101'
) AS S
PIVOT
(
SUM(Importo)
FOR Mese IN ([Gennaio], [Febbraio], [Marzo])
) AS P;In produzione preferisco spesso usare il numero del mese per evitare ordinamenti alfabetici e dipendenze dalla lingua della sessione. DATENAME può infatti restituire nomi diversi in base alle impostazioni linguistiche di SQL Server.
Un altro dettaglio facile da trascurare è la presenza di colonne extra nella sorgente. PIVOT considera come dimensioni di raggruppamento tutte le colonne non usate nell’aggregazione e nella rotazione. Una colonna inattesa, come IdTransazione, può quindi frammentare i totali e produrre molte più righe del previsto. La regola pratica che seguo è semplice: selezionare esplicitamente le colonne, evitando SELECT *.
Pivot statico o dinamico quale scegliere
Il pivot statico è la scelta migliore quando l’elenco delle colonne è noto e cambia raramente. È facile da leggere, controllare e inserire in una vista. Il limite è evidente: ogni nuovo mese, stato o categoria richiede una modifica alla query.
Quando le colonne dipendono dai dati, serve SQL dinamico. Un esempio essenziale è questo:
DECLARE @Colonne nvarchar(max);
DECLARE @Sql nvarchar(max);
SELECT @Colonne =
STRING_AGG(QUOTENAME(Mese), ',')
FROM
(
SELECT DISTINCT Mese
FROM dbo.Vendite
WHERE DataVendita >= '20260101'
AND DataVendita < '20270101'
) AS M;
SET @Sql = N'
SELECT Area, ' + @Colonne + N'
FROM
(
SELECT Area, Mese, Importo
FROM dbo.Vendite
WHERE DataVendita >= @Dal
AND DataVendita < @Al
) AS S
PIVOT
(
SUM(Importo)
FOR Mese IN (' + @Colonne + N')
) AS P;';
EXEC sys.sp_executesql
@Sql,
N'@Dal date, @Al date',
@Dal = '20260101',
@Al = '20270101';QUOTENAME protegge i nomi trasformati in identificatori SQL, mentre sp_executesql consente di passare i filtri come parametri. Non concatenerei mai direttamente valori inseriti dall’utente nella query, perché si aprirebbe un problema di SQL injection.
| Approccio | Quando usarlo | Vantaggio | Limite |
|---|---|---|---|
| Pivot statico | Colonne note in anticipo | Leggibile e prevedibile | Richiede manutenzione |
| Pivot dinamico | Categorie variabili | Si adatta ai dati | Più complesso da testare |
| Aggregazione con CASE | Pochi indicatori fissi | Grande controllo sulla logica | Query più lunga con molte colonne |
Quando CASE è più adatto di PIVOT
Non considero PIVOT automaticamente la soluzione migliore. Per tre o quattro colonne, l’aggregazione condizionale può essere più chiara e permette di applicare condizioni diverse a ogni indicatore:
SELECT
Area,
SUM(CASE WHEN Mese = 'Gennaio' THEN Importo ELSE 0 END) AS [Gennaio],
SUM(CASE WHEN Mese = 'Febbraio' THEN Importo ELSE 0 END) AS [Febbraio],
SUM(CASE WHEN Mese = 'Marzo' THEN Importo ELSE 0 END) AS [Marzo]
FROM dbo.Vendite
GROUP BY Area
ORDER BY Area;Con CASE posso, per esempio, sommare solo le vendite approvate a gennaio e tutte le vendite a febbraio, oppure calcolare nella stessa query importi, quantità e margini. Per questo, quando la logica del report è articolata, preferisco spesso questa forma anche se PIVOT sembra più elegante.
La scelta dipende quindi dall’obiettivo. PIVOT rende compatta la rotazione di una dimensione, mentre CASE offre maggiore libertà sulle condizioni. Entrambe le tecniche possono produrre lo stesso risultato, ma non hanno la stessa leggibilità quando il report cresce.
UNPIVOT, errori comuni e prestazioni
UNPIVOT compie l’operazione inversa. Trasforma colonne come Gennaio, Febbraio e Marzo in righe, creando una struttura più vicina al modello tabellare originale. È utile quando si riceve un file già organizzato come report e lo si deve caricare in una tabella analitica.
Gli errori che incontro più spesso sono questi:
- Colonna dimenticata nell’elenco IN. Un valore presente nei dati ma assente dalla lista non apparirà nel risultato.
-
Aggregazione sbagliata. Usare
SUMquando servirebbeCOUNTaltera il significato del report. -
NULL interpretati come zero. Un valore mancante non sempre significa un importo pari a zero, quindi la sostituzione con
COALESCEva fatta solo se il modello lo permette. - Raggruppamento involontario. Colonne extra nella sorgente possono creare righe duplicate o totali spezzati.
- SQL dinamico senza controllo. Nomi di colonna e valori devono essere validati e quotati correttamente.
Sulle prestazioni, filtro e aggrego il più presto possibile, ma senza complicare inutilmente la query. Un indice può aiutare quando supporta le colonne usate nei filtri e nel raggruppamento, mentre l’indice giusto dipende dalla cardinalità e dal piano di esecuzione. L’uso ripetuto di PIVOT e UNPIVOT nella stessa istruzione può peggiorare le prestazioni, perciò separerei i passaggi con una tabella temporanea quando il dataset è grande o la query diventa difficile da ottimizzare.
Per verificare il risultato non mi fermo al fatto che la query venga eseguita. Confronto sempre il totale prima e dopo la trasformazione, controllo alcune combinazioni area-mese e osservo il piano di esecuzione. Un report dall’aspetto corretto può comunque contenere aggregazioni duplicate.
Una regola pratica per portare il pivot in produzione
Userei PIVOT quando devo consegnare una vista leggibile, con una dimensione ben definita e un numero controllato di colonne. Per una tabella operativa o per un modello destinato a crescere, manterrei invece i dati in formato normalizzato e rimanderei la rotazione al livello di reporting.
La sequenza più solida è filtrare, selezionare, aggregare e solo dopo ruotare. Con questa disciplina, il pivot resta uno strumento semplice e affidabile, invece di diventare una query fragile piena di eccezioni.
Il punto decisivo non è scegliere la sintassi più corta, ma stabilire quale forma serve davvero a chi consumerà il dato. Una tabella larga è ottima per leggere un andamento mensile; una tabella con una riga per evento è quasi sempre migliore per archiviare, filtrare e alimentare nuove analisi.
