Torna all'elenco degli articoli Articoli
Tempo di lettura: 12 minuti

La clausola RANGE nelle funzioni SQL Window: 5 esempi pratici

Che cos'è un CAMPO nelle funzioni SQL a finestra? Cinque esempi pratici vi mostreranno come e quando usarla.

La clausola RANGE viene utilizzata piuttosto raramente nelle funzioni SQL a finestra. Non so perché, forse le persone non sono abituate a usarla. È un peccato, perché è tutt'altro che una clausola inutile; può essere molto utile e ve lo mostrerò in cinque esempi.

Sintassi della clausola RANGE

Quando parlo della clausola RANGE, mi riferisco a quella utilizzata nelle funzioni di SQL window, che hanno la seguente sintassi:

OVER (   
  [ <PARTITION BY clause> ]
  [ <ORDER BY clause> ]
  [ <ROW or RANGE clause> ]
) 

Osservando la sintassi sopra riportata, si nota che sia ROW che RANGE possono far parte della funzione finestra. La sintassi è la seguente:

[<clausola RANGE> TRA <inizio expr> E <fine expr>].

La finestra predefinita senza ORDER BY è l'intera partizione. Ma quando si usa l'ORDER BY, la finestra predefinita è RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.

È normale dimenticare questa sintassi, soprattutto se è nuova o se non la si usa molto spesso. Non esitate a tenere aperto il Cheat Sheet di Window Functions mentre leggete questo articolo.

Siete pronti a tuffarvi? Perfetto! Innanzitutto, parliamo di RANGE e ROW.

Le clausole RANGE e ROW sono uguali?

No, non lo sono. Tuttavia, hanno lo stesso scopo: specificare i punti iniziali e finali all'interno della partizione, con l'obiettivo di limitare le righe. Tuttavia, ogni clausola lo fa in modo diverso. La clausola ROW lo fa specificando un numero fisso di righe che precedono o seguono la riga corrente.

La clausola RANGE, invece, limita le righe in modo logico, specificando l'intervallo di valori in relazione al valore della riga corrente.

Quali tipi di dati si possono usare nella clausola RANGE?

Con la clausola RANGE è possibile utilizzare due tipi di dati: i tipi numerici e i tipi data/ora.

È possibile utilizzare i tipi di dati numerici e la clausola RANGE in quasi tutti i database più diffusi. Purtroppo, Microsoft SQL Server non la supporta.

Per quanto riguarda i tipi di dati data/ora, solo pochissimi database popolari supportano l'uso di questi tipi di dati con la clausola RANGE. Si tratta di PostgreSQL, MySQL e Oracle DB.

Passiamo ora agli esempi e vediamo come funziona in pratica RANGE! Se non conoscete le funzioni finestra di SQL, forse dovreste prima verificare cosa sono le funzioni finestra e come si confrontano con le funzioni aggregate. Tornate quando avete finito di leggere e continueremo.

Esempio 1 - Calcolo del totale progressivo

I dati con cui lavorerò sono contenuti nella tabella revenue. Le colonne sono:

  • id - L'ID della data e la chiave primaria (PK) della tabella.
  • date - La data del ricavo.
  • revenue_amount - L'importo delle entrate.

Il compito è quello di calcolare i totali correnti delle entrate utilizzando la clausola RANGE. Per prima cosa, non utilizziamo l'SQL. Se si dispone dei seguenti dati, quale sarà il totale progressivo (noto anche come somma cumulativa)?

iddaterevenue_amountrunning_total
12021-05-0112,573.2512,573.25
22021-05-0211,348.2223,921.47
32021-05-0214,895.1338,816.60
42021-05-0314,388.1453,204.74
52021-05-0418,847.5472,052.28

Il totale progressivo per il 2021-05-01 è uguale al ricavo per quella data: 12.573,25. Questo perché non ci sono righe precedenti da includere nel calcolo. Poi viene il 2021-05-02. Il totale corrente è costituito dalle entrate di oggi sommate a quelle del giorno precedente: 11,348.22 + 12,573.25 = 23,921.47.

Si noti che c'è un'altra riga con un importo di entrate diverso per il 2021-05-02. Forse si tratta di un'altra filiale, paese, prodotto o altro. Funziona allo stesso modo: 14.895,13 + 23.921,47 = 38.816,60. (La clausola RANGE funziona anche se ci sono più righe con la stessa data). Segue il 2021-05-03. Il totale corrente per questa data sarà 14.388,14 + 38.816,60 = 53.204,74. Infine, il totale corrente per il 2021-05-04 sarà 18.847,54 + 53.204 = 72.052,28.

Come si fa a fare lo stesso usando la clausola RANGE? Si potrebbe fare così:

SELECT
  id,
  date,
  revenue_amount,
  SUM(revenue_amount) OVER (
    ORDER BY date
    RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_total
FROM revenue;

Ho utilizzato la funzione SUM() sulla colonna revenue_amount; questa è l'operazione necessaria per ottenere il totale corrente. Affinché la funzione SUM() diventi una funzione finestra, è necessaria la clausola OVER(). Il calcolo della funzione finestra viene eseguito in ordine crescente; questo perché voglio assicurarmi che le entrate vengano sommate dalla data più vecchia a quella più recente. Poi viene la clausola RANGE. Limita la finestra alle date precedenti la data corrente (BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) e alla data corrente. Queste sono le righe che verranno incluse nel calcolo del totale progressivo.

Ecco cosa si ottiene eseguendo il codice:

iddaterevenue_amountrunning_total
12021-05-0112,573.2512,573.25
22021-05-0211,348.2238,816.60
32021-05-0214,895.1338,816.60
42021-05-0314,388.1453,204.74
52021-05-0418,847.5472,052.28
62021-05-059,845.2981,897.57
72021-05-0614,574.5696,472.13
82021-05-0711,500.63107,972.76
92021-05-0816,897.21124,869.97
102021-05-099,634.56134,504.53
112021-05-1014,255.87148,760.40
122021-05-1111,248.33160,008.73
132021-05-1221,489.22181,497.95
142021-05-1314,448.65195,946.60
152021-05-1415,517.22211,463.82
162021-05-156,874.23218,338.05
172021-05-1612,500.00230,838.05
182021-05-179,784.33240,622.38
192021-05-1815,321.89255,944.27
202021-05-1912,235.50268,179.77
212021-05-2022,222.22290,401.99
212021-05-2114,800.65305,202.64
222021-05-225,894.12311,096.76
232021-05-2318,845.69329,942.45
242021-05-249,966.66339,909.11
252021-05-2513,250.69353,159.80
262021-05-264,987.56358,147.36
272021-05-2717,784.25375,931.61
282021-05-2812,567.45388,499.06
292021-05-2919,874.26408,373.32
302021-05-3015,489.36423,862.68
312021-05-3114,987.55438,850.23

Si noti che quando ci sono più valori per una data (2021-05-02) il codice includerà entrambe le righe nel calcolo del totale corrente per quella data. Ecco perché nella colonna running_total c'è 38.816,60 per quella data.

Esempio 2 - Calcolo della media mobile

Vediamo ora come utilizzare la clausola RANGE con i tipi di dati data/ora - e senza la colonna id come aiuto! Utilizzeremo una tabella leggermente modificata, che ora si chiama revenue_per_shop. Contiene i dati sulle entrate di due negozi. Le colonne sono:

  • date - La data del ricavo.
  • shop - Il nome del negozio.
  • revenue_amount - L'importo delle entrate di quel negozio in quella data.

Prima di tutto, assicuriamoci di aver capito cos'è una media mobile. Una media mobile a due giorni comprende il giorno corrente e il giorno precedente. Ecco alcuni esempi di dati che illustrano il funzionamento di una media mobile:

daterevenue_amountmoving_avg
2021-05-0112,573.2512,573.25
2021-05-0211,348.2211,960.74
2021-05-0314,388.1412,868.18
2021-05-0418,847.5416,617.84

La media mobile a due giorni per il 2021-05-01 è il fatturato giornaliero stesso: 12,573.25. Questo perché non ci sono altre righe da includere nel calcolo. Il calcolo per il 2021-05-02 include due date: (12.573,25 + 11.348,22)/2 = 11.960,74. Le altre righe seguono la stessa logica delle due date: la data corrente e la data precedente.

Come si fa a calcolare la stessa metrica per ogni negozio separatamente? In questo modo:

SELECT
  shop,
  date,
  revenue_amount,
  AVG(revenue_amount) OVER (
    PARTITION BY shop
    ORDER BY date ASC
    RANGE BETWEEN INTERVAL '1' DAY PRECEDING AND CURRENT ROW
  ) AS moving_avg
FROM revenue_per_shop;

Il codice seleziona innanzitutto alcune colonne dalla tabella. Poi viene la parte divertente. Uso la funzione AVG() sulla colonna revenue_amount perché voglio il ricavo medio. Anche in questo caso, si tratta di una funzione finestra, quindi deve avere la clausola OVER(). Uso la funzione PARTITION BY per specificare la colonna su cui voglio aggregare i dati; è la colonna negozio perché voglio la media mobile per ogni negozio separatamente. L'operazione è ancora una volta ordinata per data. Nella clausola RANGE mi limito a specificare quali righe includere nel calcolo. Poiché sto lavorando con le date, otterrò la data precedente dichiarando: BETWEEN INTERVAL '1' DAY PRECEDING.

Il lavoro con i dati di data e ora varia da un database all'altro. In alcuni database potrebbe essere necessario scrivere in questo modo: RANGE BETWEEN 1 DAY PRECEDING AND CURRENT ROW. Il risultato dovrebbe comunque essere lo stesso:

shopdaterevenue_amountmoving_avg
Shop 12021-05-0112,573.2512,573.25
Shop 12021-05-0214,388.1413,480.70
Shop 12021-05-039,845.2912,116.72
Shop 12021-05-0411,500.6310,672.96
Shop 12021-05-059,634.5610,567.60
Shop 12021-05-0611,248.3310,441.45
Shop 12021-05-0714,448.6512,848.49
Shop 12021-05-086,874.2310,661.44
Shop 12021-05-099,784.338,329.28
Shop 12021-05-1012,235.5011,009.92
Shop 12021-05-1114,800.6513,518.08
Shop 12021-05-1218,845.6916,823.17
Shop 12021-05-1313,250.6916,048.19
Shop 12021-05-1417,784.2515,517.47
Shop 12021-05-1519,874.2618,829.26
Shop 22021-05-0111,348.2211,348.22
Shop 22021-05-0218,847.5415,097.88
Shop 22021-05-0314,574.5616,711.05
Shop 22021-05-0416,897.2115,735.89
Shop 22021-05-0514,255.8715,576.54
Shop 22021-05-0621,489.2217,872.55
Shop 22021-05-0715,517.2218,503.22
Shop 22021-05-0812,500.0014,008.61
Shop 22021-05-0915,321.8913,910.95
Shop 22021-05-1022,222.2218,772.06
Shop 22021-05-115,894.1214,058.17
Shop 22021-05-129,966.667,930.39
Shop 22021-05-134,987.567,477.11
Shop 22021-05-1412,567.458,777.51
Shop 22021-05-1515,489.3614,028.41

Media mobile per database che non supportano l'uso di RANGE con i tipi di dati data/ora

Cosa fare se il database non supporta l'uso di RANGE con i tipi di dati data/ora? C'è un modo per "ingannare" il database; anzi, probabilmente ci sono diversi modi. Ecco un modo che calcola la differenza tra ogni data e il 2021-05-01 (cioè la prima data nei dati). Si ottiene la differenza come numero intero, che può essere utilizzato al posto della data. Questo è il codice:

SELECT
  shop,
  date,
  revenue_amount,
  date - '2021_05_01' AS day_difference,
  AVG(revenue_amount) OVER (
    PARTITION BY shop
    ORDER BY (date - '2021_05_01')
    RANGE BETWEEN 1 PRECEDING AND CURRENT ROW
  ) AS moving_avg
FROM revenue_per_shop;

Ho intenzionalmente incluso la colonna day_difference nel risultato, in modo che possiate capire la logica. Ho utilizzato la stessa differenza nella clausola ORDER BY in modo da poter utilizzare un numero intero con la clausola RANGE. La media mobile è la stessa dell'esempio precedente; verificate voi stessi. (Per risparmiare spazio mostro solo le prime cinque righe).

shopdaterevenue_amountday_differencemoving_avg
Shop 12021-05-0112,573.25012,573.25
Shop 12021-05-0214,388.14113,480.70
Shop 12021-05-039,845.29212,116.72
Shop 12021-05-0411,500.63310,672.96
Shop 12021-05-059,634.56410,567.60

Esempio 3 - Trovare l'ultimo valore all'interno di un intervallo

L'uso della clausola RANGE consente di trovare l'ultimo valore all'interno di un intervallo definito. Ad esempio, utilizzando la tabella revenue_by_shopposso ottenere l'ultimo valore per ogni negozio separatamente. Per ultimo valore, in questo caso, si intende l'ultimo dato disponibile, ovvero il fatturato del 2021-05-15. Come si ottengono questi dati?

Utilizzando la clausola RANGE, naturalmente:

SELECT
  shop,
  date,
  revenue_amount,
  LAST_VALUE(revenue_amount) OVER (
    PARTITION BY shop
    ORDER BY date
    RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  ) AS last_value
FROM revenue_per_shop;

La funzione finestra che ho utilizzato questa volta è LAST_VALUE(). Ancora una volta, la uso sulla colonna revenue_amount. Ho partizionato i dati per negozio, come in precedenza. Ho ordinato i dati per data, come in precedenza. Per ottenere l'ultimo valore, ho usato RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING. Ricordate che l'intervallo predefinito con la clausola ORDER BY è RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Se non lo cambiate, otterrete un risultato sbagliato. Il risultato corretto è:

shopdaterevenue_amountlast_value
Shop 12021-05-0112,573.2519,874.26
Shop 12021-05-0214,388.1419,874.26
Shop 12021-05-039,845.2919,874.26
Shop 12021-05-0411,500.6319,874.26
Shop 12021-05-059,634.5619,874.26
Shop 12021-05-0611,248.3319,874.26
Shop 12021-05-0714,448.6519,874.26
Shop 12021-05-086,874.2319,874.26
Shop 12021-05-099,784.3319,874.26
Shop 12021-05-1012,235.5019,874.26
Shop 12021-05-1114,800.6519,874.26
Shop 12021-05-1218,845.6919,874.26
Shop 12021-05-1313,250.6919,874.26
Shop 12021-05-1417,784.2519,874.26
Shop 12021-05-1519,874.2619,874.26
Shop 22021-05-0111,348.2215,489.36
Shop 22021-05-0218,847.5415,489.36
Shop 22021-05-0314,574.5615,489.36
Shop 22021-05-0416,897.2115,489.36
Shop 22021-05-0514,255.8715,489.36
Shop 22021-05-0621,489.2215,489.36
Shop 22021-05-0715,517.2215,489.36
Shop 22021-05-0812,500.0015,489.36
Shop 22021-05-0915,321.8915,489.36
Shop 22021-05-1022,222.2215,489.36
Shop 22021-05-115,894.1215,489.36
Shop 22021-05-129,966.6615,489.36
Shop 22021-05-134,987.5615,489.36
Shop 22021-05-1412,567.4515,489.36
Shop 22021-05-1515,489.3615,489.36

Esempio 4 - Trovare il numero di elementi all'interno di un intervallo

Ecco un altro modo divertente e utile di utilizzare la clausola RANGE. Come trovare il numero di volte in cui il ricavo giornaliero di un negozio è stato compreso tra 1.000 (dollari, euro...) al di sotto e al di sopra del suo valore attuale?

Questo codice potrebbe essere d'aiuto:

SELECT
  shop,
  date,
  revenue_amount,
  COUNT(*) OVER (
    ORDER BY revenue_amount ASC
    RANGE BETWEEN 1000 PRECEDING AND 1000 FOLLOWING
  ) AS number_of_days
FROM revenue_per_shop;

Utilizzo la funzione COUNT() window. Poiché non mi interessa separare le entrate per negozi, non c'è PARTITION BY. Il conteggio verrà eseguito in ordine crescente in base all'importo delle entrate. L'intervallo è definito da RANGE BETWEEN 1000 PRECEDING AND 1000 FOLLOWING.

Ecco cosa restituirà il codice:

shopdaterevenue_amountnumber_of_days
Shop 22021-05-134,987.562
Shop 22021-05-115,894.123
Shop 12021-05-086,874.232
Shop 12021-05-059,634.564
Shop 12021-05-099,784.334
Shop 12021-05-039,845.294
Shop 22021-05-129,966.664
Shop 12021-05-0611,248.334
Shop 22021-05-0111,348.224
Shop 12021-05-0411,500.635
Shop 12021-05-1012,235.507
Shop 22021-05-0812,500.006
Shop 22021-05-1412,567.455
Shop 12021-05-0112,573.255
Shop 12021-05-1313,250.694
Shop 22021-05-0514,255.875
Shop 12021-05-0214,388.146
Shop 12021-05-0714,448.656
Shop 22021-05-0314,574.568
Shop 12021-05-1114,800.658
Shop 22021-05-0915,321.897
Shop 22021-05-1515,489.365
Shop 22021-05-0715,517.225
Shop 22021-05-0416,897.212
Shop 12021-05-1417,784.252
Shop 12021-05-1218,845.692
Shop 22021-05-0218,847.542
Shop 12021-05-1519,874.261
Shop 22021-05-0621,489.222
Shop 22021-05-1022,222.222

Vi spiego cosa vi dice questo risultato. Se prendiamo la prima riga, il risultato nella colonna number_of_days è 2. Ci sono due casi in cui il ricavo è compreso tra 3.987,56 e 5.987,56. Perché questo intervallo? Le entrate per il 2021-05-13 sono 4.987,56. Quindi 4.987,56 - 1.000 = 3.987,56 e 4.987,56 + 1.000 = 5.987,56. Volete verificare il risultato? Quali sono le due istanze comprese in questo intervallo? Ovviamente i primi due:

shopdaterevenue_amountnumber_of_days
Shop 22021-05-134,987.562
Shop 22021-05-115,894.123

Vogliamo controllare la seconda riga? Dice che ci sono tre istanze tra 4.894,12 e 6.894,12 - queste tre:

shopdaterevenue_amountnumber_of_days
Shop 22021-05-134,987.562
Shop 22021-05-115,894.123
Shop 12021-05-086,874.232

La stessa logica si applica al resto dei dati.

Esempio 5 - Trovare il valore massimo

Questo non è così semplice come può sembrare: non sto parlando del valore massimo comune. Per trovarlo, non sarebbe necessaria la clausola RANGE. Ma che ne dite di trovare il valore massimo (o le entrate, in questo caso) in cinque giorni? Questi cinque giorni includono la data corrente, fino a tre giorni prima e un giorno dopo la data corrente. Probabilmente, dopo tutti questi esempi di utilizzo di RANGE, conoscete già la logica. Ecco la mia soluzione:

SELECT
  shop,
  date,
  revenue_amount,
  MAX(revenue_amount) OVER (
    ORDER BY DATE
    RANGE BETWEEN INTERVAL '3' DAY PRECEDING AND INTERVAL '1' DAY FOLLOWING
  ) AS max_revenue
FROM revenue_per_shop;

Uso la funzione MAX() come funzione finestra. Ancora una volta, la utilizzo con la colonna revenue_amount. Non c'è PARTITION BY nella clausola OVER() perché non mi interessa separare i dati a nessun livello. Definire l'intervallo non è difficile: RANGE BETWEEN INTERVAL '3' DAY PRECEDING AND INTERVAL '1' DAY FOLLOWING. Questo includerà la data corrente, tre giorni prima e un giorno dopo. Ecco il risultato:

shopdaterevenue_amountmax_revenue
Shop 12021-05-0112,573.2518,847.54
Shop 22021-05-0111,348.2218,847.54
Shop 12021-05-0214,388.1418,847.54
Shop 22021-05-0218,847.5418,847.54
Shop 12021-05-039,845.29 18,847.54
Shop 22021-05-0314,574.5618,847.54
Shop 12021-05-0411,500.6318,847.54
Shop 22021-05-0416,897.2118,847.54
Shop 12021-05-059,634.56 21,489.22
Shop 22021-05-0514,255.8721,489.22
Shop 12021-05-0611,248.3321,489.22
Shop 22021-05-0621,489.2221,489.22
Shop 22021-05-0715,517.2221,489.22
Shop 12021-05-0714,448.6521,489.22
Shop 22021-05-0812,500.0021,489.22
Shop 12021-05-086,874.23 21,489.22
Shop 22021-05-0915,321.8922,222.22
Shop 12021-05-099,784.33 22,222.22
Shop 12021-05-1012,235.5022,222.22
Shop 22021-05-1022,222.2222,222.22
Shop 12021-05-1114,800.6522,222.22
Shop 22021-05-115,894.12 22,222.22
Shop 22021-05-129,966.66 22,222.22
Shop 12021-05-1218,845.6922,222.22
Shop 12021-05-1313,250.6922,222.22
Shop 22021-05-134,987.56 22,222.22
Shop 12021-05-1417,784.2519,874.26
Shop 22021-05-1412,567.4519,874.26
Shop 22021-05-1515,489.3619,874.26
Shop 12021-05-1519,874.2619,874.26

Controlliamo il risultato per 2021-05-05 - segnato in rosa. L'intervallo è contrassegnato in giallo. Per ottenere il ricavo massimo in questo intervallo, SQL confronterà i valori: 14,388.14, 18,847.54, 9,845.29, 14,574.56, 11,500.63, 16,897.21, 9,634.56, 14,255.87, 11,248.33, 21,489.22. Qual è il più alto? È 21.489,22.

Dopo aver imparato a trovare il valore massimo utilizzando la clausola RANGE, ho raggiunto il numero massimo di esempi previsti per questo articolo. Se volete altri esempi di funzioni finestra, potete sempre leggere questo articolo.

RANGE ha davvero una vasta gamma di usi, non è vero?

Credo che questi cinque esempi mostrino una buona gamma di possibilità della clausola RANGE. Non è l'unica cosa che si può fare con essa. Il suo utilizzo dipende dai dati a disposizione e probabilmente da un po' di immaginazione. Di questa clausola non si parla molto spesso, ed è un peccato. Consiglio a tutti di impararla. Potrebbe far risparmiare tempo nella creazione di soluzioni alternative. La clausola RANGE è elegante e non è molto complicata.

Dove si può imparare a conoscere RANGE? Nel nostro corso Window Functions , naturalmente. Non imparerete solo la clausola RANGE, ma ogni aspetto delle funzioni della finestra. Oppure potete usare il corso per esercitarvi in SQL. Se volete saperne di più, in questo articolo troverete tutte le informazioni sul corso e su ciò che offre.