18th Jul 2022 Tempo di lettura: 12 minuti La clausola RANGE nelle funzioni SQL Window: 5 esempi pratici Tihomir Babic sql imparare sql Window Functions RANGE Indice Sintassi della clausola RANGE Le clausole RANGE e ROW sono uguali? Quali tipi di dati si possono usare nella clausola RANGE? Esempio 1 - Calcolo del totale progressivo Esempio 2 - Calcolo della media mobile Media mobile per database che non supportano l'uso di RANGE con i tipi di dati data/ora Esempio 3 - Trovare l'ultimo valore all'interno di un intervallo Esempio 4 - Trovare il numero di elementi all'interno di un intervallo Esempio 5 - Trovare il valore massimo RANGE ha davvero una vasta gamma di usi, non è vero? 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. Tags: sql imparare sql Window Functions RANGE