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

Che cos'è la clausola OVER() in SQL?

Le funzioni finestra sono una delle risorse più potenti di SQL, ma non vengono utilizzate di frequente dallo sviluppatore medio di SQL. In questo articolo spiegheremo come è possibile definire diversi tipi di finestre utilizzando la clausola OVER.

La clausola OVER è essenziale per le funzioni finestra di SQL. Come le funzioni di aggregazione, le funzioni finestra eseguono calcoli basati su un insieme di record, ad esempio trovando lo stipendio medio di un gruppo di dipendenti.

In alcuni casi, le funzioni aggregate non possono essere utilizzate perché fanno collassare tutti i singoli record in un gruppo; ciò rende impossibile fare riferimento a valori specifici (come lo stipendio di un dipendente del gruppo). In queste situazioni, le funzioni finestra sono preferibili perché non comprimono le righe; è possibile fare riferimento a un valore di colonna a livello di riga e al valore aggregato.

Ci sono altri scenari in cui le funzioni finestra sono utili. Ad esempio, può essere necessario eseguire un'operazione aritmetica che coinvolga una singola colonna e un calcolo basato su un insieme di righe. Un esempio reale è il calcolo della differenza tra lo stipendio medio del reparto e lo stipendio di ciascun dipendente del reparto.

Quando si utilizzano le funzioni finestra, è fondamentale definire l'insieme di record in cui verrà calcolata la funzione. Questo insieme di record è chiamato cornice della finestra; lo definiamo utilizzando la clausola SQL OVER.

In questo articolo mostreremo le query SQL utilizzando il database di una piccola azienda di orologi di lusso. L'azienda memorizza le informazioni sulle vendite in una tabella chiamata sales:

sale_day sale_month sale_time branch article quantity revenue
2021-08-11 AUG 11:00 New York Rolex P1 1 3000.00
2021-08-14 AUG 11:20 New York Rolex P1 2 6000.00
2021-08-17 AUG 10:00 Paris Omega 100 3 4000.00
2021-08-19 AUG 10:00 London Omega 100 1 1300.00
2021-07-17 JUL 09:30 Paris Cartier A1 1 2000.00
2021-07-11 JUL 10:10 New York Cartier A1 1 2000.00
2021-07-10 JUL 11:40 London Omega 100 2 2600.00
2021-07-15 JUL 10:30 London Omega 100 3 4000.00

Il window frame è un insieme di righe che dipende dalla riga corrente; pertanto, l'insieme di righe potrebbe cambiare per ogni riga elaborata dalla query. Per definire le cornici delle finestre si utilizza la clausola OVER. La sintassi è la seguente:

  OVER ([PARTITION BY columns] [ORDER BY columns])

La sottoclausola PARTITION BY definisce i criteri che i record devono soddisfare per far parte della finestra. In altre parole, PARTITION BY definisce i gruppi in cui vengono suddivise le righe; questo sarà più chiaro nel prossimo esempio di query. Infine, la clausola ORDER BY definisce l'ordine dei record nella finestra.

Vediamo la clausola SQL OVER in azione. Ecco una semplice query che restituisce la quantità totale di unità vendute per ogni articolo.

SELECT sale_day, sale_time, 
       branch, article, quantity, revenue,
       SUM(quantity) OVER (PARTITION BY article) AS total_units_sold
FROM   sales

Questa query mostrerà tutti i record della tabella sales con una nuova colonna che mostra il numero totale di unità vendute per l'articolo in questione. Possiamo ottenere la quantità di unità vendute utilizzando la funzione di aggregazione SUM, ma non potremmo mostrare i singoli record.

In questa query, la sottoclausola OVER PARTITION BY article indica che la cornice della finestra è determinata dai valori della colonna article; tutti i record con lo stesso valore article saranno in un gruppo. Di seguito è riportato il risultato di questa query:

sale day sale time branch article quantity revenue total units sold
2021-07-11 10:10 New York Cartier A1 1 2000.00 2
2021-07-17 9:30 Paris Cartier A1 1 2000.00 2
2021-08-19 10:00 London Omega 100 1 1300.00 9
2021-07-15 10:30 London Omega 100 3 4000.00 9
2021-08-17 10:00 Paris Omega 100 3 4000.00 9
2021-07-10 11:40 London Omega 100 2 2600.00 9
2021-08-11 11:00 New York Rolex P1 1 3000.00 3
2021-08-14 11:20 New York Rolex P1 2 6000.00 3

La colonna total_units_sold del report è stata ottenuta tramite l'espressione:

SUM(quantity) OVER (PARTITION BY article) total_units_sold

Per i lettori che desiderano approfondire l'argomento, suggerisco i seguenti due articoli: Qual è la differenza tra GROUP BY e PARTITION BY e Window Functions in SQL Server: Prima parte: la clausola OVER()

La clausola OVER di SQL in azione

Per ogni articolo, supponiamo di voler confrontare la quantità totale di questo articolo venduto in ogni mese del 2021 con la quantità totale di questo articolo venduto nell'intero anno. Per farlo, creeremo un semplice report con le colonne article, month, units_sold_month e units_sold_year. La query è:

SELECT DISTINCT article,
       EXTRACT('month' FROM sale_day) AS month,
       SUM(quantity) OVER (PARTITION BY article, sale_month) AS units_sold_month,
       SUM(quantity) OVER (PARTITION BY article) AS units_sold_year
FROM  sales 
WHERE EXTRACT('YEAR' FROM sale_day) = 2021
ORDER BY article, month

I risultati della query sono:

article month units_sold_month units_sold_year
Cartier A1 7 2 2
Omega 100 7 5 9
Omega 100 8 4 9
Rolex P1 8 3 3

In questo caso, abbiamo calcolato le unità totali vendute utilizzando due diverse granularità di raggruppamento: mese e anno. La prima clausola OVER...

OVER (PARTITION BY article, sale_month) 

... ci permette di ottenere il numero di unità di ogni articolo vendute in un mese. La seconda clausola OVER...

OVER (PARTITION BY article)

... ci permette di calcolare il numero totale di unità di un determinato articolo vendute nell'intero anno.

Nella prossima query, aggiungeremo semplicemente la colonna month_percentage per mostrare la percentuale che uno specifico mese occupa all'interno del totale annuale. Possiamo calcolarla con la seguente query:

SELECT DISTINCT article,
       EXTRACT('month' FROM sale_day) as month,
       SUM(quantity) OVER (PARTITION BY article, sale_month) AS units_sold_month,
       SUM(quantity) OVER (PARTITION BY article) AS units_sold_year,
       ( ( SUM(quantity) OVER (PARTITION BY article, sale_month)::decimal /
           SUM(quantity) OVER (PARTITION BY article)::decimal ) * 100
       ) AS month_percentage
FROM sales 
WHERE extract('YEAR' FROM sale_day) = 2021
ORDER BY article, month

Si noti che nella query precedente non abbiamo utilizzato clausole OVER diverse, ma abbiamo semplicemente riutilizzato le stesse funzioni della finestra e calcolato una percentuale. I risultati sono visibili qui sotto:

article month units_sold_month units_sold_year month_percentage
Cartier A1 7 2 2 100.00
Omega 100 7 5 9 55.55
Omega 100 8 4 9 45.44
Rolex P1 8 3 3 100.00

Ora creeremo un altro report che analizza l'andamento delle varie filiali. Vogliamo visualizzare le colonne branch e month. Abbiamo anche bisogno di calcoli per ottenere il:

  • Entrate totali per quel mese.
  • Entrate raggruppate per filiale e mese.
  • Entrate medie mensili delle filiali.
  • Differenza tra le entrate di ogni filiale e le entrate medie mensili.
SELECT DISTINCT branch,
       EXTRACT('month' FROM sale_day) AS month,
       SUM(revenue) OVER (PARTITION BY sale_month) AS total_revenue_month,
       SUM(revenue) OVER (PARTITION BY branch, sale_month) AS branch_revenue_month,
      
 -- Next column is the branch average revenue in the current month
	 ( 
		  SUM(revenue) OVER (PARTITION BY sale_month)::decimal 
		   / 
		  (SELECT COUNT(DISTINCT branch) FROM sales)::decimal 
	 ) AS average_month_branch,
 
 -- Next column is the difference between branch revenue and average branch revenue

        SUM(revenue) OVER (PARTITION BY branch, sale_month) -
	 ( 
		  SUM(revenue) OVER (PARTITION BY sale_month)::decimal 
		   / 
		  (SELECT COUNT(DISTINCT branch) FROM sales)::decimal 
	 ) AS gap_branch_average

FROM sales 
WHERE extract('YEAR' from sale_day) = 2021
ORDER BY branch, month

Ancora una volta abbiamo utilizzato solo due clausole OVER, ma abbiamo usato espressioni aritmetiche diverse per ottenere determinati valori. Abbiamo usato ...

SUM(revenue) OVER (PARTITION BY sale_month) 

... per calcolare il fatturato totale del mese, ma lo abbiamo anche utilizzato in un'espressione aritmetica per ottenere il fatturato medio mensile delle filiali.

Abbiamo usato ...

SUM(revenue) OVER (PARTITION BY branch, sale_month) 

... per calcolare le entrate mensili della filiale e la differenza tra le entrate mensili della filiale e la media.

La tabella successiva è il risultato della query. Si noti che la colonna gap_branch_average può contenere numeri positivi o negativi. Un numero negativo indica che il fatturato mensile di questa filiale è stato inferiore al fatturato medio.

Branch Month total_revenue_month branch_revenue_month average_month_branch gap_branch_average
London 7 10600 6600 3533.33 3066.66
London 8 14300 1300 4766.66 -3466.66
New York 7 10600 2000 3533.33 -1533.33
New York 8 14300 9000 4766.66 4233.33
Paris 7 10600 2000 3533.33 -1533.33
Paris 8 14300 4000 4766.66 -766.66

Per ulteriori informazioni sulle funzioni finestra in SQL, suggerisco SQL Window Function Example With Explanations, un articolo di base sulle funzioni finestra. Per i lettori più esperti, Come classificare le righe all'interno di una partizione in SQL mostra come creare classifiche nei report utilizzando la funzione finestra RANK().

La clausola OVER e l'analitica Window Functions

Nelle query precedenti abbiamo usato le funzioni finestra per confrontare i numeri mensili (rispettivamente fatturato e unità vendute) con quelli annuali. In questa sezione utilizzeremo le finestre ordinate, che ci permettono di scegliere un record nella finestra in base alla sua posizione. Ad esempio, possiamo scegliere il primo record della finestra, il record precedente a quello attuale o il record successivo a quello attuale. Queste funzioni analitiche della finestra forniscono una grande potenza espressiva all'SQL.

Nella seguente query, mostreremo l'aumento/diminuzione dei ricavi per la stessa filiale in due mesi contigui. A tale scopo, è necessario calcolare la differenza tra le entrate del mese corrente e quelle del mese precedente. A tale scopo, è necessaria la funzione analitica LAG(), che può ottenere il valore di una colonna da una riga precedente a quella corrente.

WITH branch_month_sales AS (
	SELECT    DISTINCT
		    branch,
		    EXTRACT('MONTH' FROM sale_day) AS month,
		    SUM(revenue) OVER (PARTITION BY branch, sale_month ) AS revenue
	FROM sales
)
SELECT branch, 
	month,
	revenue AS revenue_current_month,
	LAG(revenue) OVER (PARTITION BY branch ORDER BY month) AS revenue_prev_month,
	revenue - LAG(revenue) OVER (PARTITION BY branch ORDER BY month) AS revenue_delta
FROM branch_month_sales
ORDER BY branch, month

In questa query, abbiamo utilizzato un'espressione di tabella comune (CTE) chiamata branch_month_sales per memorizzare le entrate totali di ogni filiale e di ogni mese. Poi abbiamo scritto una seconda query che utilizza la funzione finestra LAG() per ottenere le entrate del mese precedente (utilizzando le informazioni di branch_month_sales). Si noti che la finestra è ordinata per mese.

Ecco i risultati:

Branch Month revenue_current_month revenue_prev_month revenue_delta
London 7 6600 null null
London 8 1300 6600 -5300
New York 7 2000 null null
New York 8 9000 2000 7000
Paris 7 2000 null null
Paris 8 4000 2000 2000

In tutte le query presentate in questo articolo, abbiamo utilizzato solo alcune funzioni finestra. Esistono molte altre funzioni finestra in SQL. Ecco un elenco di ciascuna di esse:

function syntax return value
AVG() AVG(expression) The average within the OVER partition.
COUNT() COUNT() The number of rows within the OVER partition.
MAX() MAX(expression) The maximum value of a column or expression for each partition.
MIN() MIN(expression) The minimum value of a column or expression for each partition.
SUM() SUM(expression) The total of all values in a column within a partition.
ROW_NUMBER() ROW_NUMBER() Assigns a unique number to each row within a partition. Rows with identical values are given row different numbers.
RANK() RANK() Ranks rows by column values within a partition. Gaps and tied rankings are permitted.
DENSE_RANK() DENSE_RANK() Ranks row by column values within a partition. There are no gaps in the ranking, but tied rankings are permitted.
PERCENT_RANK() PERCENT_RANK() Assigns a percentile ranking number to each row in a partition. To calculate a value in the [0, 1] interval, we use (rank - 1) / (total number of rows - 1).
CUME_DIST() CUME_DIST() Shows the cumulative distribution of a value within a group of values, i.e. the number of rows with values less than or equal to the current row’s value divided by the total number of rows.
LEAD() LEAD(expr, offset, default) The value of the row n number of rows after the current row. The offset and default arguments are optional; it will return the next row value by default.
LAG() LAG(expr, offset, default) The value of the row n number of rows before the current row. The offset and default arguments are optional; it will return the previous row value by default.
NTILE() NTILE(n) Divides rows within a partition into n groups and assigns each row a group number.
FIRST_VALUE() FIRST_VALUE(expr) The value for the first row within the window frame.
LAST_VALUE() LAST_VALUE(expr) The value for the last row within the window frame.
NTH_VALUE() NTH_VALUE(expr, n) The value for the n-th row within the window frame.

Per saperne di più sulla clausola OVER di SQL

In questo articolo abbiamo trattato la clausola OVER e le sottoclausole PARTITION BY e ORDER BY.

Se volete continuare a conoscere le funzioni finestra, ci sono un paio di articoli che vorrei condividere. Il primo è 8 Best SQL Window Function Articles, che vi indirizzerà ad altri ottimi articoli. Il secondo è un foglio informativo sulle funzioni finestra che include sintassi, esempi e immagini; è il mio articolo preferito sulle funzioni finestra.

Per chi vuole approfondire, suggerisco il nostro corso interattivo Window Functions SQL. Se volete migliorare le vostre conoscenze di SQL in generale, provate il corso SQL dalla A alla Z. È una panoramica completa di tutto ciò che dovete sapere per lavorare efficacemente con SQL.