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

Come usare la clausola SQL PARTITION BY con OVER

Al centro di ogni chiamata di funzione finestra c'è una clausola OVER che definisce come vengono costruite le finestre dei record. All'interno della clausola OVER, può essere presente una sottoclausola opzionale PARTITION BY che definisce i criteri per identificare i record da includere in ciascuna finestra. Continuate a leggere e fate un passo importante per accrescere le vostre competenze in SQL!

Che cos'è la clausola PARTITION BY in SQL?

L'espressione SQL PARTITION BY è una sottoclausola della clausola OVER, utilizzata in quasi tutte le invocazioni di funzioni finestra come AVG(), MAX() e RANK(). Come molti lettori probabilmente sapranno, le funzioni finestra operano su cornici di finestre che sono insiemi di righe che possono essere diverse per ogni record del risultato della query. È qui che entra in gioco la sottoclausola SQL PARTITION BY, utilizzata per definire quali record devono far parte della cornice della finestra associata a ciascun record del risultato.

Questo articolo spiega la sottoclausola SQL PARTITION BY e il suo utilizzo con alcuni esempi. Poiché è profondamente legata alle funzioni finestra, si consiglia di leggere prima alcuni articoli sulle funzioni finestra, come "SQL Window Function Example With Explanations", dove si trovano molti esempi. Se volete saperne di più sulle funzioni finestra, c'è anche un interessante articolo con molti puntatori ad altri articoli sulle funzioni finestra.

La prima cosa su cui concentrarsi è la sintassi. Ecco come utilizzare la clausola SQL PARTITION BY:

SELECT
    ,
     OVER(PARTITION BY  [ORDER BY ])
FROM table;

Vediamo un esempio che utilizza la clausola PARTITION BY. Utilizzeremo la seguente tabella chiamata car_list_prices:

car_makecar_modelcar_typecar_price
FordMondeopremium18200
RenaultFuegosport16500
CitroenCactuspremium19000
FordFalconlow cost8990
FordGalaxystandard12400
RenaultMeganestandard14300
CitroenPicassopremium23400

Per ogni autovettura vogliamo ottenere la marca, il modello, il prezzo, il prezzo medio di tutte le autovetture e il prezzo medio dello stesso tipo di autovettura (per avere un'idea più precisa del prezzo di una determinata autovettura rispetto alle altre). Ecco la query:

SELECT
    car_make,
    car_model,
    car_price,
    AVG(car_price) OVER() AS "overall average price",
    AVG(car_price) OVER (PARTITION BY car_type) AS "car type average price"
FROM car_list_prices

Il risultato della query è il seguente:

car_makecar_modelcar_priceoverall average pricecar type average price
FordMondeo1820016112.858990.00
RenaultFuego1650016112.8520200.00
CitroenCactus1900016112.8520200.00
FordFalcon899016112.8520200.00
FordGalaxy1240016112.8516500.00
RenaultMegane1430016112.8513350.00
CitroenPicasso2340016112.8513350.00

La query di cui sopra utilizza due funzioni finestra. La prima viene utilizzata per calcolare il prezzo medio di tutte le auto presenti nel listino. Utilizza la funzione finestra AVG() con una clausola OVER vuota, come si vede nell'espressione seguente:

AVG(car_price) OVER() AS "overall average price"

La seconda funzione finestra viene utilizzata per calcolare il prezzo medio di una specifica car_type vettura come standard, premium, sport, ecc. In questo caso si utilizza una clausola OVER con una sottoclausola PARTITION BY come si vede in questa espressione:

AVG(car_price) OVER (PARTITION BY car_type) AS "car type average price"

Le funzioni della finestra sono molto potenti, vero? Se volete imparare di più facendo esercizi ben preparati, vi suggerisco il corso Window Functionsdove potrete imparare a conoscere e ad utilizzare le funzioni finestra nei database SQL.

Approfondimento della clausola SQL PARTITION BY

La clausola GROUP BY raggruppa un insieme di record in base a dei criteri. Ciò consente di applicare una funzione (ad esempio, AVG() o MAX()) a gruppi di record per ottenere un risultato per gruppo.

A titolo di esempio, si supponga di voler ottenere il prezzo medio e il prezzo massimo per ogni marca. Utilizzare la seguente query:

SELECT car_make,
       AVG(car_price) AS average_price,
       MAX(car_price) AS top_price
FROM   car_list_prices
GROUP BY car_make

Ecco il risultato di questa query:

car_makeaverage_pricetop_price
Ford1319618200
Renault1540016500
Citroen2120023400

Rispetto alle funzioni finestra, GROUP BY comprime i singoli record in un gruppo. Di conseguenza, non è possibile fare riferimento a nessun campo di un singolo record, cioè solo alle colonne della clausola GROUP BY.

Ad esempio, si vuole creare un report con il modello, il prezzo e il prezzo medio della marca. Non è possibile farlo usando GROUP BY, perché i singoli record di ogni modello sono collassati a causa della clausola GROUP BY car_make. Per un'operazione di questo tipo, è necessario utilizzare le funzioni di finestra, come si vede nell'esempio seguente:

SELECT car_make,
       car_model,
       car_price,
       AVG(car_price) OVER (PARTITION BY car_make) AS average_make
FROM   car_list_prices

Il risultato di questa query è il seguente:

car_makecar_modelcar_priceaverage_make
CitroenPicasso2340021200
CitroenCactus1900021200
FordGalaxy1240013196
FordFalcon899013196
FordMondeo1820013196
RenaultMegane1430015400
RenaultFuego1650015400

Per chi volesse approfondire, suggerisco l'articolo "Qual è la differenza tra un GROUP BY e un PARTITION BY?" con molti esempi che utilizzano le funzioni aggregate e le funzioni finestra.

Oltre alla clausola PARTITION BY, esiste un'altra clausola chiamata ORDER BY che stabilisce l'ordine dei record all'interno della finestra. Alcune funzioni finestra richiedono una clausola ORDER BY. Ad esempio, le funzioni finestra LEAD() e LAG() richiedono che la finestra dei record sia ordinata, poiché accedono al record precedente o successivo a quello corrente.

Un riquadro di finestra è composto da diverse righe definite dai criteri della clausola PARTITION BY. Tuttavia, è possibile specificare dei limiti o dei confini alla cornice della finestra, come si vede nell'immagine seguente:

Come utilizzare il PARTITION BY di SQL con OVER

I limiti inferiori e superiori nella clausola OVER possono essere:

  • UNBOUNDED PRECEDING
  • n PRECEDING
  • CURRENT ROW
  • n FOLLOWING
  • UNBOUNDED FOLLOWING

Quando non si specifica alcun limite in una clausola OVER, la cornice della finestra viene costruita sulla base di alcuni valori limite predefiniti. Questi dipendono dalla sintassi utilizzata per chiamare la funzione finestra. La tabella seguente mostra i limiti predefiniti della cornice della finestra.

Syntax usedFirst Row in WindowLast Row in Window
Just empty OVER() clauseUNBOUNDED PRECEDINGUNBOUNDED FOLLOWING
OVER(PARTITION BY …)UNBOUNDED PRECEDINGUNBOUNDED FOLLOWING
OVER(PARTITION BY … ORDER BY …)UNBOUNDED PRECEDINGCURRENT ROW

Esiste un articolo dettagliato intitolato "SQL Window Functions Cheat Sheet" in cui si possono trovare molti dettagli sulla sintassi ed esempi sui diversi limiti della cornice della finestra.

La clausola SQL PARTITION BY in azione

In questa sezione vengono mostrati alcuni esempi della clausola SQL PARTITION BY. Tutti si basano sulla tabella paris_london_flightsutilizzata da una compagnia aerea per analizzare i risultati commerciali di questa rotta per gli anni 2018 e 2019. Ecco un sottoinsieme dei dati:

aircraft_makeaircarft_modelflight_numberscheduled_departurereal_departurescheduled_arrivalnum_of_passengerstotal_revenue
Boeing757 300FLP0032019-01-30 15:00:002019-01-30 15:00:002019-01-30 15:00:0026082630.10
Boeing737 200FLP0032019-02-01 15:00:002019-02-01 15:10:002019-02-01 15:55:0019558459.34
AirbusA500FLP0032019-02-01 15:00:002019-02-01 15:03:002019-02-01 15:03:5531291570.87
AirbusA500FLP0012019-10-28 05:00:002019-10-28 05:04:002019-10-28 05:55:0029887943.00
Boeing737 200FLP0022019-10-28 09:00:002019-10-28 09:00:002019-10-28 09:55:0017856342.45

Esempio 1

La prima query genera un report che include il sito flight_number, aircraft_model con la quantità di passeggeri trasportati e le entrate totali. La query è riportata di seguito:

SELECT DISTINCT
       flight_number,
       aircraft_model,
	SUM(num_of_passengers) OVER (PARTITION BY flight_number, aircraft_model)
                                                            AS total_passengers,
	SUM(total_revenue) OVER (PARTITION BY flight_number, aircraft_model)
                                                            AS total_revenue
FROM paris_london_flights
ORDER BY flight_number, aircraft_model;

Poiché il totale dei passeggeri trasportati e il totale dei ricavi sono generati per ogni possibile combinazione di flight_number e aircraft_model, utilizziamo la seguente clausola PARTITION BY per generare un insieme di record con lo stesso numero di volo e modello di aeromobile:

OVER (PARTITION BY flight_number, aircraft_model)

Quindi, per ogni insieme di record, applichiamo le funzioni finestra SUM(num_of_passengers) e SUM(total_revenue) per ottenere le metriche total_passengers e total_revenue mostrate nel prossimo set di risultati.

flight_numberaircraft_modeltotal_passengerstotal_revenue
FLP001737 200204816016060.82
FLP001757 300183895361126.23
FLP001Airbus A5005387215892165.58
FLP002737 200216606297197.71
FLP002757 300168694951475.86
FLP002Airbus A5005462716004812.16
FLP003737 200200985874892.44
FLP003757 300157084573379.28
FLP003Airbus A5005753316712475.04

Esempio 2

Nella prossima query, mostriamo l'evoluzione del business confrontando le metriche di un mese con quelle del mese precedente. Creiamo un report utilizzando le funzioni finestra per mostrare la variazione mensile dei passeggeri e dei ricavi.

WITH year_month_data AS (
  SELECT DISTINCT
	   EXTRACT(YEAR FROM scheduled_departure) AS year,
	   EXTRACT(MONTH FROM scheduled_departure) AS month,
	   SUM(number_of_passengers)
              OVER (PARTITION BY EXTRACT(YEAR FROM scheduled_departure),
                                  EXTRACT(MONTH FROM scheduled_departure)
                   ) AS passengers
   FROM  paris_london_flights
  ORDER BY 1, 2
)
SELECT  year,
        month,
	 passengers,
	 LAG(passengers) OVER (ORDER BY year, month) passengers_previous_month,
	 passengers - LAG(passengers) OVER (ORDER BY year, month) AS passengers_delta
FROM year_month_data;

Nella query di cui sopra, utilizziamo una clausola WITH per generare una CTE (CTE è l'acronimo di common table expressions ed è un tipo di query per generare una tabella virtuale che può essere utilizzata nel resto della query). I dati vengono inseriti in una tabella virtuale chiamata year_month_datache ha 3 colonne: year, month, e passengers con il totale dei passeggeri trasportati nel mese.

Quindi, la seconda query (che prende in input la CTE year_month_data come input) genera il risultato della query. La colonna passeggeri contiene il totale dei passeggeri trasportati associati al record corrente. Con la funzione finestra LAG(passenger), si ottiene il valore della colonna passeggeri del record precedente a quello attuale. ORDER BY year and mont h:

LAG(passengers) OVER (ORDER BY year, month)
passengers_previous_month

Si ottiene il numero di passeggeri dal record precedente, corrispondente al mese precedente. Poi, abbiamo il numero di passeggeri per il mese corrente e per quello precedente. Infine, nell'ultima colonna, si calcola la differenza tra i due valori per ottenere la variazione mensile dei passeggeri.

yearmonthpassengerspassengers_previous_monthpassengers_delta
20181211469nullnull
20191247231146913254
201922253624723-2187
2019324994225362458
201942440824994-586
201952399824408-410
201962379323998-205
2019724816237931023
201982433424816-482
201992371924334-615
20191024989237191270
2019112437124989-618
201912108724371-23284

Esempio 3

Per l'ultimo esempio, esaminiamo i ritardi dei voli. Vogliamo ottenere diverse medie di ritardo per spiegare le ragioni dei ritardi.

Utilizziamo una CTE per calcolare una colonna chiamata month_delay con il ritardo medio per ogni mese e otteniamo il modello di aereo. Poi, nella query principale, otteniamo le diverse medie, come si vede di seguito:

WITH paris_london_delays AS (
  SELECT DISTINCT
	   aircraft_model,
	   EXTRACT(YEAR FROM scheduled_departure) AS year,
	   EXTRACT(MONTH FROM scheduled_departure) AS month,
	   AVG(real_departure - scheduled_departure) AS month_delay
   FROM  paris_london_flights
   GROUP BY 1, 2, 3
)
SELECT  DISTINCT
     aircraft_model,
     year,
     month,
     month_delay AS monthly_avg_delay,
     AVG(month_delay) OVER (PARTITION BY aircraft_model, year) AS year_avg_delay,
     AVG(month_delay) OVER (PARTITION BY year) AS year_avg_delay_all_models,
     AVG(month_delay) OVER (PARTITION BY aircraft_model, year 
                               ORDER BY month
                               ROWS BETWEEN 3 PRECEDING AND CURRENT ROW
                            ) AS rolling_average_last_4_months

FROM paris_london_delays
ORDER BY 1,2,3

Questa query calcola diverse medie. La prima è la media per modello di aeromobile e anno, che è molto chiara. La seconda è la media per anno di tutti i modelli di aereo. Si noti che nella clausola PARTITION BY viene utilizzata solo la colonna anno. La terza e ultima media è la media mobile, in cui si utilizzano gli ultimi 3 mesi e il mese corrente (cioè la riga) per calcolare la media con la seguente espressione:

AVG(month_delay) OVER (PARTITION BY aircraft_model, year
                               ORDER BY month
                               ROWS BETWEEN 3 PRECEDING AND CURRENT ROW
                           ) AS rolling_average_last_4_months

La clausola ROWS BETWEEN 3 PRECEDING AND CURRENT ROW in PARTITION BY limita il numero di righe (cioè di mesi) da includere nella media: i 3 mesi precedenti e il mese corrente. Di seguito è possibile vedere un risultato parziale di questa query:

aircraft_modelyearmonthmonth_delayyear_avg_delayyear_avg_delay_all_modelsrolling_average_last_4_months
737 20020181200:02:13.8400:02:13.8400:03:13.7000:02:13.84
737 2002019100:02:16.8000:02:36.5900:02:34.1200:02:16.80
737 2002019200:02:35.0000:02:36.5900:02:34.1200:02:25.90
737 2002019300:01:38.4000:02:36.5900:02:34.1200:02:10.06
737 2002019400:04:00.0000:02:36.5900:02:34.1200:02:37.55
737 2002019500:03:12.7200:02:36.5900:02:34.1200:02:51.53
737 2002019600:02:21.4200:02:36.5900:02:34.1200:02:48.13

L'articolo "The RANGE Clause in SQL Window Functions: 5 Practical Examples" spiega come definire un sottoinsieme di righe nella finestra utilizzando RANGE invece di ROWS, con diversi esempi. Un altro articolo interessante è "Common SQL Window Functions: Using Partitions With Ranking Functions" in cui viene trattata in dettaglio la clausola PARTITION BY.

Il potere di Window Functions e della clausola SQL PARTITION BY

Le funzioni finestra sono una risorsa molto potente del linguaggio SQL e la clausola SQL PARTITION BY svolge un ruolo centrale nel loro utilizzo. In questo articolo abbiamo illustrato il funzionamento di questa clausola e mostrato diversi esempi utilizzando diverse sintassi.

Prima di chiudere, vi suggerisco un Advanced SQL corso, dove potrete andare oltre le nozioni di base e diventare maestri di SQL. Se volete saperne di più sulla clausola OVER, c'è un articolo completo sull'argomento: "Come definire un riquadro finestra in SQL Window Functions." Migliorate le vostre competenze e accrescete il vostro patrimonio!