SQL Window Functions: Ranking, Totali Progressivi e Analisi
Published: 2026-03-24
Le window functions eseguono calcoli su un insieme di righe correlate alla riga corrente — senza comprimere il risultato in una singola riga di output come fa GROUP BY. Sono una delle funzionalità più potenti dell’SQL moderno, supportate in SQLite (3.25+), PostgreSQL, MySQL (8.0+), SQL Server e Oracle.
Dati di esempio
Usiamo due tabelle in tutti gli esempi:
Tabella employees:
| id | name | department | hire_date | salary |
|---|---|---|---|---|
| 1 | Alice | Engineering | 2023-01-15 | 95000 |
| 2 | Bob | Engineering | 2023-06-01 | 85000 |
| 3 | Carol | Engineering | 2024-03-10 | 85000 |
| 4 | Dave | Sales | 2023-02-20 | 72000 |
| 5 | Eve | Sales | 2023-09-05 | 68000 |
| 6 | Frank | Sales | 2024-01-15 | 65000 |
| 7 | Grace | Marketing | 2023-04-01 | 82000 |
| 8 | Henry | Marketing | 2024-06-01 | 70000 |
Tabella monthly_sales:
| id | employee_id | month | revenue |
|---|---|---|---|
| 1 | 1 | 2026-01 | 22000 |
| 2 | 1 | 2026-02 | 25000 |
| 3 | 1 | 2026-03 | 28000 |
| 4 | 2 | 2026-01 | 19000 |
| 5 | 2 | 2026-02 | 21000 |
| 6 | 2 | 2026-03 | 20000 |
| 7 | 3 | 2026-01 | 15000 |
| 8 | 3 | 2026-02 | 17000 |
| 9 | 3 | 2026-03 | 19000 |
| 10 | 4 | 2026-01 | 18000 |
| 11 | 4 | 2026-02 | 22000 |
| 12 | 4 | 2026-03 | 17000 |
| 13 | 5 | 2026-01 | 14000 |
| 14 | 5 | 2026-02 | 16000 |
| 15 | 5 | 2026-03 | 19000 |
| 16 | 6 | 2026-01 | 11000 |
| 17 | 6 | 2026-02 | 13000 |
| 18 | 6 | 2026-03 | 15000 |
| 19 | 7 | 2026-01 | 9000 |
| 20 | 7 | 2026-02 | 12000 |
| 21 | 7 | 2026-03 | 14000 |
| 22 | 8 | 2026-01 | 7000 |
| 23 | 8 | 2026-02 | 9000 |
| 24 | 8 | 2026-03 | 11000 |
Sintassi delle window functions
Ogni funzione finestra usa la clausola OVER per definire su quali righe operare:
function_name(...) OVER (
[PARTITION BY column]
[ORDER BY column]
[frame_clause]
)
- PARTITION BY — divide le righe in gruppi (come GROUP BY, ma mantiene tutte le righe)
- ORDER BY — definisce l’ordinamento delle righe all’interno di ogni partizione
- Frame clause — limita la finestra a un intervallo di righe relativo alla riga corrente
ROW_NUMBER()
ROW_NUMBER() assegna un numero sequenziale univoco a ogni riga all’interno di una partizione. I numeri partono sempre da 1, senza salti o ex aequo.
Esempio: Numerare tutti i dipendenti per stipendio
Esempio: Numerare i dipendenti all’interno di ogni reparto
Usando PARTITION BY, la numerazione riparte da 1 per ogni reparto:
RANK() e DENSE_RANK()
RANK() e DENSE_RANK() gestiscono gli ex aequo in modo diverso da ROW_NUMBER():
- ROW_NUMBER() — sempre univoco, criterio di spareggio arbitrario
- RANK() — lo stesso valore riceve lo stesso rango, poi salta dei numeri (1, 2, 2, 4)
- DENSE_RANK() — lo stesso valore riceve lo stesso rango, senza salti (1, 2, 2, 3)
Esempio: Confrontare le tre funzioni di ranking
Bob e Carol guadagnano entrambi 85000 — osserva come ogni funzione gestisce l’ex aequo:
Esempio: Classificare i dipendenti all’interno del reparto
NTILE()
NTILE(n) distribuisce le righe in n gruppi (bucket) approssimativamente uguali, numerati da 1 a n.
Esempio: Suddividere i dipendenti in quartili di stipendio
Esempio: Suddividere i dipendenti in due fasce per reparto
LAG() e LEAD()
LAG(column, offset) accede a un valore da una riga precedente. LEAD(column, offset) accede a un valore da una riga successiva. Entrambe predefiniscono offset 1 e restituiscono NULL quando non c’è una riga.
Esempio: Confronto mese su mese del fatturato
Confronta il fatturato di ogni mese con il mese precedente per ciascun dipendente:
Esempio: Anteprima del fatturato del mese successivo
Window functions di aggregazione
Le funzioni di aggregazione standard (SUM, AVG, COUNT, MIN, MAX) possono essere usate come window functions con OVER. A differenza di GROUP BY, ogni riga viene mantenuta nell’output.
Esempio: Stipendio di ogni dipendente vs media del reparto
Esempio: Percentuale del totale stipendi
Calcola quale frazione del monte salari totale rappresenta ciascun dipendente:
Esempio: Totale progressivo e conteggio cumulativo
Totali progressivi per partizione
Combina PARTITION BY e ORDER BY per calcolare totali progressivi all’interno dei gruppi.
Esempio: Fatturato cumulativo per dipendente
Esempio: Classifica del fatturato mensile tra tutti i dipendenti
Trova come si è classificato ogni dipendente ogni mese:
Window frame
Un window frame definisce esattamente quali righe, relative alla riga corrente, sono incluse nel calcolo. La clausola di frame viene dopo ORDER BY all’interno di OVER.
Sintassi
ROWS BETWEEN <start> AND <end>
Limiti comuni:
UNBOUNDED PRECEDING— dalla prima riga della partizioneN PRECEDING— N righe prima della riga correnteCURRENT ROW— la riga correnteN FOLLOWING— N righe dopo la riga correnteUNBOUNDED FOLLOWING— fino all’ultima riga della partizione
Esempio: Media mobile a 2 mesi
Esempio: Minimo e massimo progressivi del fatturato
FIRST_VALUE e LAST_VALUE
FIRST_VALUE(column) restituisce il primo valore nel window frame. LAST_VALUE(column) restituisce l’ultimo valore. Attenzione: LAST_VALUE con il frame predefinito (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) vede solo fino alla riga corrente.
Esempio: Confrontare ogni mese con il primo mese
Esempio: Confrontare ogni mese con il mese migliore
Usa ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING per vedere l’intera partizione:
Esempio pratico: Top earner per reparto
Un pattern comune: usare una funzione finestra in una sottoquery, poi filtrare per rango.
Esempio: Dipendente più pagato in ogni reparto
Esempio: Mese con il fatturato più alto per dipendente
Window functions vs. GROUP BY
Una differenza chiave: GROUP BY comprime le righe in righe di riepilogo, mentre le window functions mantengono ogni riga e aggiungono una colonna calcolata.
Esempio: Confronto affiancato
GROUP BY restituisce una riga per reparto:
Le window functions mantengono tutte le righe:
Casi d’uso comuni per le window functions
- Classifiche e leaderboard — ROW_NUMBER, RANK, DENSE_RANK per query top-N
- Totali progressivi — SUM cumulativa per monitoraggio finanziario e delle scorte
- Confronto periodo su periodo — LAG/LEAD per analisi mese su mese o anno su anno
- Medie mobili — Per “smussare” i dati con AVG basata su frame
- Analisi per percentili — NTILE per suddividere i dati in gruppi uguali
- Deduplicazione — ROW_NUMBER per scegliere una riga per gruppo
- Analisi dei gap — LAG per rilevare sequenze mancanti o variazioni anomale
Suggerimenti sulle prestazioni
- Indicizza le colonne di ORDER BY — Le window functions ordinano internamente i dati; gli indici aiutano
- Riduci al minimo le partizioni — Un numero elevato di partizioni aumenta il carico computazionale
- Riusa le definizioni di finestra — Molti database supportano le clausole
WINDOWper evitare di ripetere la stessa specificaOVER - Filtra dopo il windowing — Le window functions vengono eseguite prima di
WHERE, quindi usa sottoquery o CTE per filtrare per rango - Evita frame non necessari — Specifica clausole di frame solo quando ti serve un comportamento non predefinito
Riepilogo
- Le window functions calcolano valori su righe correlate senza comprimere il result set
- ROW_NUMBER, RANK, DENSE_RANK assegnano posizioni ordinali alle righe
- LAG e LEAD accedono a valori da righe precedenti o successive
- NTILE distribuisce le righe in bucket di dimensioni uguali
- FIRST_VALUE e LAST_VALUE restituiscono i valori ai limiti della finestra
- Le funzioni di aggregazione (SUM, AVG, COUNT, MIN, MAX) con
OVERmantengono tutte le righe mentre calcolano metriche a livello di gruppo - PARTITION BY crea gruppi indipendenti; ORDER BY definisce la sequenza delle righe
- I window frame (
ROWS BETWEEN ... AND ...) perfezionano quali righe sono incluse - Usa sottoquery per filtrare in base ai risultati delle window functions (ad es. top-N per gruppo)
- Supportate in SQLite 3.25+, PostgreSQL, MySQL 8.0+, SQL Server e Oracle
Prova a modificare gli esempi per sperimentare con partizioni, ordinamenti e clausole di frame diversi, per vedere come le window functions possono semplificare le tue query analitiche!