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
Esplora il dataset in DbGate Lite

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 partizione
  • N PRECEDING — N righe prima della riga corrente
  • CURRENT ROW — la riga corrente
  • N FOLLOWING — N righe dopo la riga corrente
  • UNBOUNDED 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

  1. Indicizza le colonne di ORDER BY — Le window functions ordinano internamente i dati; gli indici aiutano
  2. Riduci al minimo le partizioni — Un numero elevato di partizioni aumenta il carico computazionale
  3. Riusa le definizioni di finestra — Molti database supportano le clausole WINDOW per evitare di ripetere la stessa specifica OVER
  4. Filtra dopo il windowing — Le window functions vengono eseguite prima di WHERE, quindi usa sottoquery o CTE per filtrare per rango
  5. Evita frame non necessari — Specifica clausole di frame solo quando ti serve un comportamento non predefinito
  • 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 OVER mantengono 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!