Retour à la liste des articles Articles
10 minutes de lecture

Comment utiliser la clause SQL PARTITION BY avec OVER

Au cœur de chaque appel de fonction fenêtre se trouve une clause OVER qui définit comment les fenêtres des enregistrements sont construites. Dans la clause OVER, il peut y avoir une sous-clause facultative PARTITION BY qui définit les critères d'identification des enregistrements à inclure dans chaque fenêtre. Poursuivez votre lecture et franchissez une étape importante dans le développement de vos compétences en SQL !

Qu'est-ce que la clause PARTITION BY en SQL ?

L'expression SQL PARTITION BY est une sous-clause de la clause OVER, qui est utilisée dans presque toutes les invocations de fonctions de fenêtre comme AVG(), MAX() et RANK(). Comme de nombreux lecteurs le savent probablement, les fonctions de fenêtre opèrent sur des cadres de fenêtre qui sont des ensembles de lignes qui peuvent être différents pour chaque enregistrement du résultat de la requête. C'est là qu'intervient la sous-clause SQL PARTITION BY: elle permet de définir les enregistrements qui doivent faire partie du cadre de fenêtre associé à chaque enregistrement du résultat.

Cet article explique le SQL PARTITION BY et ses utilisations avec des exemples. Comme il est profondément lié aux fonctions de fenêtre, vous pouvez d'abord lire certains articles sur les fonctions de fenêtre, comme "SQL Window Function Example With Explanations", où vous trouverez de nombreux exemples. Si vous souhaitez en savoir plus sur les fonctions de fenêtre, il existe également un article intéressant contenant de nombreux pointeurs vers d'autres articles sur les fonctions de fenêtre.

La première chose à laquelle il faut s'intéresser est la syntaxe. Voici comment utiliser la clause SQL PARTITION BY:

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

Examinons un exemple qui utilise une clause PARTITION BY. Nous allons utiliser la table suivante appelée car_list_prices:

car_makecar_modelcar_typecar_price
FordMondeopremium18200
RenaultFuegosport16500
CitroenCactuspremium19000
FordFalconlow cost8990
FordGalaxystandard12400
RenaultMeganestandard14300
CitroenPicassopremium23400

Pour chaque voiture, nous voulons obtenir la marque, le modèle, le prix, le prix moyen sur l'ensemble des voitures, et le prix moyen sur le même type de voiture (pour avoir une meilleure idée de la façon dont le prix d'une voiture donnée se compare aux autres voitures). Voici la requête :

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

Le résultat de la requête est le suivant :

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 requête ci-dessus utilise deux fonctions de fenêtre. La première est utilisée pour calculer le prix moyen de toutes les voitures de la liste de prix. Elle utilise la fonction fenêtre AVG() avec une clause OVER vide comme nous le voyons dans l'expression suivante :

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

La deuxième fonction de fenêtre est utilisée pour calculer le prix moyen d'un car_type spécifique comme standard, premium, sport, etc. C'est ici que nous utilisons une clause OVER avec une sous-clause PARTITION BY comme nous le voyons dans cette expression :

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

Les fonctions de fenêtre sont assez puissantes, non ? Si vous souhaitez en apprendre davantage en faisant des exercices bien préparés, je vous suggère le cours Fonctions de fenêtragequi vous permettra d'apprendre et de vous familiariser avec l'utilisation des fonctions de fenêtre dans les bases de données SQL.

Approfondir la clause SQL PARTITION BY

La clause GROUP BY regroupe un ensemble d'enregistrements en fonction de critères. Cela nous permet d'appliquer une fonction (par exemple, AVG() ou MAX()) à des groupes d'enregistrements pour obtenir un résultat par groupe.

Par exemple, supposons que nous voulions obtenir le prix moyen et le prix le plus élevé pour chaque marque. Utilisez la requête suivante :

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

Voici le résultat de cette requête :

car_makeaverage_pricetop_price
Ford1319618200
Renault1540016500
Citroen2120023400

Par rapport aux fonctions de fenêtre, GROUP BY réduit les enregistrements individuels en un groupe. Par conséquent, vous ne pouvez pas faire référence à un champ d'enregistrement individuel, c'est-à-dire que seules les colonnes de la clause GROUP BY peuvent être référencées.

Par exemple, supposons que vous souhaitiez créer un rapport contenant le modèle, le prix et le prix moyen de la marque. Vous ne pouvez pas le faire en utilisant GROUP BY, car les enregistrements individuels de chaque modèle sont réduits en raison de la clause GROUP BY car_make. Pour ce genre de chose, vous devez utiliser les fonctions de fenêtre, comme nous le voyons dans l'exemple suivant :

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

Le résultat de cette requête est le suivant :

car_makecar_modelcar_priceaverage_make
CitroenPicasso2340021200
CitroenCactus1900021200
FordGalaxy1240013196
FordFalcon899013196
FordMondeo1820013196
RenaultMegane1430015400
RenaultFuego1650015400

Pour ceux qui veulent aller plus loin, je suggère l'article ""Quelle est la différence entre un GROUP BY et un PARTITION BY ?" avec de nombreux exemples utilisant les fonctions agrégat et fenêtre.

En plus de la clause PARTITION BY, il existe une autre clause appelée ORDER BY qui établit l'ordre des enregistrements dans le cadre de la fenêtre. Certaines fonctions de fenêtre nécessitent une clause ORDER BY. Par exemple, les fonctions de fenêtre LEAD() et LAG() ont besoin que la fenêtre d'enregistrement soit ordonnée car elles accèdent à l'enregistrement précédent ou suivant à partir de l'enregistrement actuel.

Un cadre de fenêtre est composé de plusieurs lignes définies par les critères de la clause PARTITION BY. Cependant, nous pouvons spécifier des limites ou des bornes au cadre de la fenêtre comme nous le voyons dans l'image suivante :

Comment utiliser la méthode SQL PARTITION BY avec OVER

Les limites inférieures et supérieures de la clause OVER peuvent être :

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

Lorsque nous ne spécifions aucune limite dans une clause OVER, son cadre de fenêtre est construit sur la base de certaines valeurs limites par défaut. Elles dépendent de la syntaxe utilisée pour appeler la fonction de fenêtre. Le tableau suivant montre les limites par défaut du cadre de la fenêtre.

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

Il existe un article détaillé intitulé "SQL Fonctions de fenêtrage Cheat Sheet" dans lequel vous trouverez de nombreux détails syntaxiques et exemples concernant les différentes limites du cadre de fenêtre.

La clause SQL PARTITION BY en action

Dans cette section, nous présentons quelques exemples de la clause SQL PARTITION BY. Tous sont basés sur la table paris_london_flightsutilisée par une compagnie aérienne pour analyser les résultats commerciaux de cette route pour les années 2018 et 2019. Voici un sous-ensemble de ces données :

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

Exemple 1

La première requête génère un rapport comprenant le site flight_number, aircraft_model avec la quantité de passagers transportés, et le revenu total. La requête est présentée ci-dessous :

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;

Puisque le nombre total de passagers transportés et le revenu total sont générés pour chaque combinaison possible de flight_number et aircraft_model, nous utilisons la clause suivante PARTITION BY pour générer un ensemble d'enregistrements avec le même numéro de vol et le même modèle d'avion :

OVER (PARTITION BY flight_number, aircraft_model)

Ensuite, pour chaque ensemble d'enregistrements, nous appliquons les fonctions de fenêtre SUM(num_of_passengers) et SUM(total_revenue) pour obtenir les métriques total_passengers et total_revenue présentées dans l'ensemble de résultats suivant.

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

Exemple 2

Dans la requête suivante, nous montrons l'évolution de l'activité en comparant les mesures d'un mois à celles du mois précédent. Nous créons un rapport utilisant des fonctions de fenêtre pour montrer la variation mensuelle du nombre de passagers et des recettes.

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;

Dans la requête ci-dessus, nous utilisons une clause WITH pour générer une CTE (CTE signifie common table expressions et est un type de requête permettant de générer une table virtuelle qui peut être utilisée dans le reste de la requête). Nous introduisons des données dans une table virtuelle appelée year_month_dataqui comporte 3 colonnes : year, month, et passengers avec le total des passagers transportés au cours du mois.

Ensuite, la deuxième requête (qui prend le CTE year_month_data en entrée) génère le résultat de la requête. La colonne passagers contient le total des passagers transportés associé à l'enregistrement actuel. Avec la fonction fenêtre LAG(passenger), nous obtenons la valeur de la colonne passagers de l'enregistrement précédent à l'enregistrement courant. Nous ORDER BY year and month :

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

Il obtient le nombre de passagers de l'enregistrement précédent, correspondant au mois précédent. Ensuite, nous avons le nombre de passagers pour le mois en cours et le mois précédent. Enfin, dans la dernière colonne, on calcule la différence entre les deux valeurs pour obtenir la variation mensuelle des passagers.

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

Troisième exemple

Pour notre dernier exemple, intéressons-nous aux retards des vols. Nous voulons obtenir différentes moyennes de retards pour expliquer les raisons de ces retards.

Nous utilisons un CTE pour calculer une colonne appelée month_delay avec le retard moyen pour chaque mois et obtenir le modèle d'avion. Ensuite, dans la requête principale, nous obtenons les différentes moyennes comme nous le voyons ci-dessous :

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

Cette requête calcule plusieurs moyennes. La première est la moyenne par modèle d'avion et par année, ce qui est très clair. La seconde est la moyenne par année pour tous les modèles d'avions. Notez que nous utilisons uniquement la colonne année dans la clause PARTITION BY. La troisième et dernière moyenne est la moyenne glissante, où nous utilisons les 3 mois les plus récents et le mois en cours (c'est-à-dire la ligne) pour calculer la moyenne avec l'expression suivante :

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 clause ROWS BETWEEN 3 PRECEDING AND CURRENT ROW du site PARTITION BY restreint le nombre de lignes (c'est-à-dire de mois) à inclure dans la moyenne : les 3 mois précédents et le mois en cours. Vous pouvez voir un résultat partiel de cette requête ci-dessous :

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'article "The RANGE Clause in SQL Fonctions de fenêtrage: 5 Practical Examples" explique comment définir un sous-ensemble de lignes dans le cadre de la fenêtre en utilisant RANGE au lieu de ROWS, avec plusieurs exemples. Un autre article intéressant est "Common SQL Fonctions de fenêtrage: Using Partitions With Ranking Functions" dans lequel la clause PARTITION BY est traitée en détail.

La puissance de Fonctions de fenêtrage et de la clause SQL PARTITION BY

Les fonctions de classement sont une ressource très puissante du langage SQL, et la clause SQL PARTITION BY joue un rôle central dans leur utilisation. Dans cet article, nous avons couvert le fonctionnement de cette clause et montré plusieurs exemples utilisant différentes syntaxes.

Avant de conclure, je vous propose un SQL avancé cours, où vous pourrez aller au-delà des bases et devenir un maître du SQL. Si vous voulez en savoir plus sur la clause OVER, il existe un article complet sur le sujet : "How to Define a Window Frame in SQL Fonctions de fenêtrage." Améliorez vos compétences et développez vos atouts !