Tutoriel sur les Fonctions de Fenêtrage SQL

Dans cette leçon, nous aborderons :

  1. Introduction aux fonctions de fenêtrage

  2. Syntaxe de base du fenêtrage

  3. Les suspects habituels : SUMCOUNT, et AVG

  4. ROW_NUMBER()

  5. RANK() et DENSE_RANK()

  6. NTILE

  7. LAG et LEAD

  8. Définir un alias de fenêtre

  9. Techniques avancées de fenêtrage

Ce tutoriel utilise des données du programme Capital Bikeshare de Washington DC, qui publie des données historiques détaillées au niveau des trajets sur leur site web. Les données ont été téléchargées en février 2014, mais se limitent aux données collectées au cours du premier trimestre de 2012. Chaque ligne représente un trajet. La plupart des champs sont explicites, sauf type_utilisateur : "Inscrit" indique un abonnement mensuel au programme de partage de vélos, "Occasionnel" indique que l'utilisateur a acheté un pass de 3 jours. Les champs heure_debut et heure_fin ont été nettoyés de leur format d'origine pour correspondre au format de date SQL — ils sont stockés dans cette table sous forme de timestamps.

Introduction aux fonctions de fenêtrage

La documentation de PostgreSQL fait un excellent travail pour introduire le concept des fonctions de fenêtrage :

Une fonction de fenêtrage effectue un calcul sur un ensemble de lignes de la table qui sont liées d'une certaine manière à la ligne courante. Cela est comparable au type de calcul qui peut être fait avec une fonction d'agrégation. Mais contrairement aux fonctions d'agrégation classiques, l'utilisation d'une fonction de fenêtrage ne regroupe pas les lignes en une seule ligne de sortie — les lignes conservent leur identité séparée. En arrière-plan, la fonction de fenêtrage est capable d'accéder à plus que la ligne courante du résultat de la requête.

L'exemple le plus pratique de cela est un total cumulé :

 
SELECT duree_secondes,
       SUM(duree_secondes) OVER (ORDER BY heure_debut) AS total_cumulatif
  FROM tutoriel.vélos_partage_q1_2012

Vous pouvez voir que la requête ci-dessus crée une agrégation (total_cumulatif) sans utiliser GROUP BY. Décomposons la syntaxe et voyons comment cela fonctionne.

Syntaxe de base du fenêtrage

La première partie de l'agrégation ci-dessus, SUM(duree_secondes), ressemble beaucoup à n'importe quelle autre agrégation. L'ajout de OVER la désigne comme une fonction de fenêtrage. Vous pouvez lire l'agrégation ci-dessus comme "prendre la somme de duree_secondes sur l'ensemble du résultat, ordonné par heure_debut."

Si vous souhaitez réduire la fenêtre de l'ensemble de données à des groupes individuels dans l'ensemble de données, vous pouvez utiliser PARTITION BY pour le faire :

 
SELECT station_debut,
       duree_secondes,
       SUM(duree_secondes) OVER
         (PARTITION BY station_debut ORDER BY heure_debut)
         AS total_cumulatif
  FROM tutoriel.vélos_partage_q1_2012
 WHERE heure_debut < '2012-01-08'
 

La requête ci-dessus groupe et ordonne la requête par station_debut. Dans chaque valeur de station_debut, elle est ordonnée par heure_debut, et le total cumulatif somme la ligne courante et toutes les lignes précédentes de duree_secondes. Faites défiler jusqu'à ce que la valeur de station_debut change et vous remarquerez que total_cumulatif recommence. C'est ce qui se passe lorsque vous utilisez PARTITION BY. Si vous êtes encore perplexe par ORDER BY, il ordonne simplement par la ou les colonnes désignées de la même manière que la clause ORDER BY, sauf qu'il traite chaque partition comme séparée. Il crée également le total cumulatif — sans ORDER BY, chaque valeur sera simplement une somme de toutes les valeurs de duree_secondes dans sa station_debut respective. Essayez d'exécuter la requête ci-dessus sans ORDER BY pour vous en faire une idée :

SELECT station_debut,
       duree_secondes,
       SUM(duree_secondes) OVER
         (PARTITION BY station_debut) AS total_station_debut
  FROM tutoriel.vélos_partage_q1_2012
 WHERE heure_debut < '2012-01-08'
 

ORDER BY et PARTITION BY définissent ce que l'on appelle la "fenêtre" — le sous-ensemble ordonné de données sur lequel les calculs sont effectués.

Remarque : Vous ne pouvez pas utiliser les fonctions de fenêtrage et les agrégations standard dans la même requête. Plus précisément, vous ne pouvez pas inclure de fonctions de fenêtrage dans une clause GROUP BY.

Les suspects habituels : SUMCOUNT, et AVG

Lorsque vous utilisez des fonctions de fenêtrage, vous pouvez appliquer les mêmes agrégats que vous utiliseriez dans des circonstances normales — SUMCOUNT, et AVG. La manière la plus simple de comprendre ces fonctions est de réexécuter l'exemple précédent avec quelques fonctions supplémentaires. Par exemple :

 
 
SELECT station_debut,
       duree_secondes,
       SUM(duree_secondes) OVER
         (PARTITION BY station_debut) AS total_cumulatif,
       COUNT(duree_secondes) OVER
         (PARTITION BY station_debut) AS nombre_cumulatif,
       AVG(duree_secondes) OVER
         (PARTITION BY station_debut) AS moyenne_cumulative
  FROM tutoriel.vélos_partage_q1_2012
 WHERE heure_debut < '2012-01-08'
 

Alternativement, les mêmes fonctions avec ORDER BY :

 
SELECT station_debut,
       duree_secondes,
       SUM(duree_secondes) OVER
         (PARTITION BY station_debut ORDER BY heure_debut)
         AS total_cumulatif,
       COUNT(duree_secondes) OVER
         (PARTITION BY station_debut ORDER BY heure_debut)
         AS nombre_cumulatif,
       AVG(duree_secondes) OVER
         (PARTITION BY station_debut ORDER BY heure_debut)
         AS moyenne_cumulative
  FROM tutoriel.vélos_partage_q1_2012
 WHERE heure_debut < '2012-01-08'
 

Assurez-vous d'exécuter ces deux requêtes dans votre environnement SQL. Le prochain problème pratique est très similaire aux exemples, alors essayez de modifier le code ci-dessus plutôt que de partir de zéro.

ROW_NUMBER()

ROW_NUMBER() fait exactement ce que son nom indique — il affiche le numéro d'une ligne donnée. Il commence à 1 et numérote les lignes selon la partie ORDER BY de l'instruction de fenêtrage. ROW_NUMBER() ne nécessite pas de spécifier une variable entre les parenthèses :

 
 
SELECT station_debut,
       heure_debut,
       duree_secondes,
       ROW_NUMBER() OVER (ORDER BY heure_debut)
                    AS numero_ligne
  FROM tutoriel.vélos_partage_q1_2012
 WHERE heure_debut < '2012-01-08'
 

L'utilisation de la clause PARTITION BY vous permettra de recommencer la numérotation à 1 dans chaque partition. La requête suivante recommence le comptage pour chaque station :

 
 
 
SELECT station_debut,
       heure_debut,
       duree_secondes,
       ROW_NUMBER() OVER (PARTITION BY station_debut
                          ORDER BY heure_debut)
                    AS numero_ligne
  FROM tutoriel.vélos_partage_q1_2012
 WHERE heure_debut < '2012-01-08'

RANK() et DENSE_RANK()

RANK() est légèrement différent de ROW_NUMBER(). Si vous ordonnez par heure_debut, par exemple, il se peut que certaines stations aient des trajets avec deux heures de début identiques. Dans ce cas, ils reçoivent le même rang, alors que ROW_NUMBER() leur attribue des numéros différents. Dans la requête suivante, vous remarquez les 4ème et 5ème observations pour station_debut 31000 — elles reçoivent toutes deux un rang de 4, et le résultat suivant reçoit un rang de 6 :

 
 
 
SELECT station_debut,
       duree_secondes,
       RANK() OVER (PARTITION BY station_debut
                    ORDER BY heure_debut)
              AS rang
  FROM tutoriel.vélos_partage_q1_2012
 WHERE heure_debut < '2012-01-08'

Vous pouvez également utiliser DENSE_RANK() au lieu de RANK() selon votre application. Imaginez une situation dans laquelle trois entrées ont la même valeur. En utilisant l'une ou l'autre commande, elles recevront toutes le même rang. Pour les besoins de cet exemple, disons que c'est "2". Voici comment les deux commandes évalueraient les résultats suivants différemment :

  • RANK() donnerait aux lignes identiques un rang de 2, puis sauterait les rangs 3 et 4, donc le résultat suivant serait 5.

  • DENSE_RANK() donnerait toujours aux lignes identiques un rang de 2, mais la ligne suivante serait 3 — aucun rang ne serait sauté.

NTILE

Vous pouvez utiliser les fonctions de fenêtrage pour identifier dans quel percentile (ou quartile, ou toute autre subdivision) une ligne donnée se situe. La syntaxe est NTILE(nombre_de_tranches). Dans ce cas, ORDER BY détermine quelle colonne utiliser pour déterminer les quartiles (ou le nombre de "tranches" que vous spécifiez). Par exemple :

 
 
 
SELECT station_debut,
       duree_secondes,
       NTILE(4) OVER
         (PARTITION BY station_debut ORDER BY duree_secondes)
          AS quartile,
       NTILE(5) OVER
         (PARTITION BY station_debut ORDER BY duree_secondes)
         AS quintile,
       NTILE(100) OVER
         (PARTITION BY station_debut ORDER BY duree_secondes)
         AS percentile
  FROM tutoriel.vélos_partage_q1_2012
 WHERE heure_debut < '2012-01-08'
 ORDER BY station_debut, duree_secondes

En regardant les résultats de la requête ci-dessus, vous pouvez voir que la colonne percentile ne calcule pas exactement comme vous pourriez vous y attendre. Si vous n'aviez que deux enregistrements et que vous mesuriez les percentiles, vous vous attendriez à ce qu'un enregistrement définisse le 1er percentile, et l'autre enregistrement définisse le 100ème percentile. En utilisant la fonction NTILE, ce que vous verriez en réalité est un enregistrement dans le 1er percentile, et un dans le 2ème percentile. Vous pouvez voir cela dans les résultats pour station_debut 31000 — la colonne percentile ressemble simplement à un classement numérique. Si vous faites défiler jusqu'à station_debut 31007, vous pouvez voir qu'elle calcule correctement les percentiles car il y a plus de 100 enregistrements pour cette station. Si vous travaillez avec des fenêtres très petites, gardez cela à l'esprit et envisagez d'utiliser des quartiles ou des bandes similaires.

LAG et LEAD

Il se peut souvent qu'il soit utile de comparer des lignes à des lignes précédentes ou suivantes, surtout si vos données sont dans un ordre qui a du sens. Vous pouvez utiliser LAG ou LEAD pour créer des colonnes qui extraient des valeurs d'autres lignes — tout ce que vous avez à faire est de spécifier quelle colonne extraire et à combien de lignes de distance vous souhaitez effectuer l'extraction. LAG extrait des lignes précédentes et LEAD extrait des lignes suivantes :

sql
 
SELECT station_debut,
       duree_secondes,
       LAG(duree_secondes, 1) OVER
         (PARTITION BY station_debut ORDER BY duree_secondes) AS lag,
       LEAD(duree_secondes, 1) OVER
         (PARTITION BY station_debut ORDER BY duree_secondes) AS lead
  FROM tutoriel.vélos_partage_q1_2012
 WHERE heure_debut < '2012-01-08'
 ORDER BY station_debut, duree_secondes

Cela est particulièrement utile si vous souhaitez calculer des différences entre les lignes :

SELECT station_debut,
       duree_secondes,
       duree_secondes -LAG(duree_secondes, 1) OVER
         (PARTITION BY station_debut ORDER BY duree_secondes)
         AS difference
  FROM tutoriel.vélos_partage_q1_2012
 WHERE heure_debut < '2012-01-08'
 ORDER BY station_debut, duree_secondes
 
 

La première ligne de la colonne difference est nulle car il n'y a pas de ligne précédente à partir de laquelle on va extraire. De même, l'utilisation de LEAD créera des valeurs nulles à la fin de l'ensemble de données. Si vous souhaitez rendre les résultats un peu plus propres, vous pouvez les encapsuler dans une sous-requête pour supprimer les valeurs nulles :

SELECT *
  FROM (
    SELECT station_debut,
           duree_secondes,
           duree_secondes -LAG(duree_secondes, 1) OVER
             (PARTITION BY station_debut ORDER BY duree_secondes)
             AS difference
      FROM tutoriel.vélos_partage_q1_2012
     WHERE heure_debut < '2012-01-08'
     ORDER BY station_debut, duree_secondes
       ) sous_requete
 WHERE sous_requete.difference IS NOT NULL
 
 

Définir un alias de fenêtre

Si vous prévoyez d'écrire plusieurs fonctions de fenêtrage dans la même requête, en utilisant la même fenêtre, vous pouvez créer un alias. Prenons l'exemple NTILE ci-dessus :

SELECT station_debut,
       duree_secondes,
       NTILE(4) OVER
         (PARTITION BY station_debut ORDER BY duree_secondes)
         AS quartile,
       NTILE(5) OVER
         (PARTITION BY station_debut ORDER BY duree_secondes)
         AS quintile,
       NTILE(100) OVER
         (PARTITION BY station_debut ORDER BY duree_secondes)
         AS percentile
  FROM tutoriel.vélos_partage_q1_2012
 WHERE heure_debut < '2012-01-08'
 ORDER BY station_debut, duree_secondes
 
 

Cela peut être réécrit comme suit :

SELECT station_debut,
       duree_secondes,
       NTILE(4) OVER ntile_window AS quartile,
       NTILE(5) OVER ntile_window AS quintile,
       NTILE(100) OVER ntile_window AS percentile
  FROM tutoriel.vélos_partage_q1_2012
 WHERE heure_debut < '2012-01-08'
WINDOW ntile_window AS
         (PARTITION BY station_debut ORDER BY duree_secondes)
 ORDER BY station_debut, duree_secondes
 
 

La clause WINDOW, si elle est incluse, doit toujours venir après la clause WHERE.

Techniques avancées de fenêtrage

Vous pouvez consulter une liste complète des fonctions de fenêtrage dans Postgres (la syntaxe utilisée par Mode) dans la documentation de Postgres. Si vous utilisez des fonctions de fenêtrage sur une base de données connectée, vous devriez consulter le guide de syntaxe approprié pour votre système.

 


Récapitulatif

  • Fonctions de fenêtrage : Elles permettent d'effectuer des calculs sur un ensemble de lignes liées à la ligne courante sans regrouper les résultats en une seule ligne.

  • Syntaxe de base : Utilisez OVER pour désigner une fonction de fenêtrage et PARTITION BY pour diviser les données en groupes.

  • Agrégats courants : SUMCOUNT, et AVG peuvent être utilisés avec des fonctions de fenêtrage.

  • Numérotation des lignes : ROW_NUMBER()RANK(), et DENSE_RANK() permettent de numéroter les lignes selon un ordre spécifique.

  • Subdivision des données : NTILE permet de diviser les données en quartiles, quintiles, percentiles, etc.

  • Comparaison de lignes : LAG et LEAD permettent de comparer des lignes avec des lignes précédentes ou suivantes.

  • Alias de fenêtre : Utilisez WINDOW pour définir un alias de fenêtre et simplifier les requêtes avec plusieurs fonctions de fenêtrage.

Ce tutoriel vous a introduit aux concepts de base et avancés des fonctions de fenêtrage en SQL. Avec ces outils, vous pouvez effectuer des analyses de données plus complexes et obtenir des insights plus approfondis à partir de vos ensembles de données.