Retour à la liste des articles Articles
13 minutes de lecture

La clause RANGE dans les fonctions de fenêtre SQL : 5 exemples pratiques

Qu'est-ce qu'une clause RANGE dans les fonctions fenêtres SQL ? Cinq exemples pratiques vous montreront comment et quand l'utiliser.

La clause RANGE est utilisée assez rarement dans les fonctions de fenêtre SQL. Je ne sais pas pourquoi, peut-être que les gens n'y sont pas habitués. C'est dommage, car c'est loin d'être une clause inutile ; elle peut être très utile, et je vais vous le montrer dans cinq exemples.

Syntaxe de la clause RANGE

Lorsque je parle de la clause RANGE, je parle de celle utilisée dans les fonctions de la fenêtre SQL, dont la syntaxe est la suivante :

OVER (   
  [ <PARTITION BY clause> ]
  [ <ORDER BY clause> ]
  [ <ROW or RANGE clause> ]
) 

Lorsque vous regardez la syntaxe ci-dessus, vous voyez que ROW ou RANGE peuvent tous deux faire partie de la fonction fenêtre. Leur syntaxe est la suivante :

[<Clauses ou clause RANGE> ENTRE <Début expr> ET <Fin expr>]

Le cadre de fenêtre par défaut sans le ORDER BY est la partition entière. Mais lorsque vous utilisez le ORDER BY, le cadre de la fenêtre par défaut est RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.

Il est normal d'oublier cette syntaxe, surtout si elle est nouvelle pour vous ou si vous ne l'utilisez pas très souvent. N'hésitez pas à ouvrir votre Cheat SheetFonctions de fenêtrage pendant que vous lisez cet article.

Vous êtes prêt à vous lancer ? Très bien ! Tout d'abord, parlons de RANGE et ROW.

Les clauses RANGE et ROW sont-elles identiques ?

Non, elles ne le sont pas. Elles ont cependant le même objectif : spécifier les points de départ et d'arrivée dans la partition, dans le but de limiter les lignes. Cependant, chaque clause s'y prend différemment. La clause ROW le fait en spécifiant un nombre fixe de lignes qui précèdent ou suivent la ligne actuelle.

La clause RANGE, quant à elle, limite les lignes de manière logique ; elle spécifie la plage de valeurs par rapport à la valeur de la ligne actuelle.

Quels types de données puis-je utiliser dans la clause RANGE ?

Vous pouvez utiliser deux types de données avec la clause RANGE: les types numériques et les types date/heure.

Vous pouvez utiliser les types de données numériques et la clause RANGE dans presque toutes les bases de données courantes. Malheureusement, Microsoft SQL Server ne la prend pas en charge.

En ce qui concerne les types de données date/heure, seules quelques bases de données populaires permettent de les utiliser avec la clause RANGE. Il s'agit de PostgreSQL, MySQL et Oracle DB.

Passons maintenant aux exemples et voyons comment RANGE fonctionne en pratique ! Si les fonctions de fenêtre SQL sont nouvelles pour vous, vous devriez peut-être commencer par vérifier ce que sont les fonctions de fenêtre et comment elles se comparent aux fonctions d'agrégation. Revenez quand vous aurez fini de lire et nous continuerons.

Exemple 1 - Calculer le total courant

Les données avec lesquelles je vais travailler sont dans le tableau revenue. Les colonnes sont :

  • id - L'ID de la date et la clé primaire (PK) de la table.
  • date - La date de la recette.
  • revenue_amount - Le montant de la recette.

Votre tâche consiste à calculer les totaux courants des recettes à l'aide de la clause RANGE. Commençons par le faire sans SQL. Si vous disposez des données suivantes, quel sera le total courant (également appelé somme cumulée) ?

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

Le total courant pour le 2021-05-01 est égal aux recettes pour cette date : 12 573,25. Cela s'explique par le fait qu'il n'y a pas de lignes précédentes à inclure dans le calcul. Vient ensuite le 2021-05-02. Le total courant est le revenu d'aujourd'hui ajouté au revenu du jour précédent : 11,348.22 + 12,573.25 = 23,921.47.

Remarquez qu'il y a une autre ligne avec un montant de recettes différent pour 2021-05-02. Peut-être est-ce pour une autre branche, un autre pays, un autre produit ou autre. Cela fonctionne de la même manière : 14 895,13 + 23 921,47 = 38 816,60. (La clause RANGE fonctionne même s'il y a plusieurs lignes avec la même date). Vient ensuite le 2021-05-03. Le total courant pour cette date sera de 14 388,14 + 38 816,60 = 53 204,74. Enfin, le total courant pour le 2021-05-04 sera de 18 847,54 + 53 204 = 72 052,28.

Comment faire de même en utilisant la clause RANGE? Cela pourrait se faire de la manière suivante :

SELECT
  id,
  date,
  revenue_amount,
  SUM(revenue_amount) OVER (
    ORDER BY date
    RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_total
FROM revenue;

J'ai utilisé la fonction SUM() sur la colonne revenue_amount; c'est l'opération nécessaire pour obtenir le total courant. Pour que la fonction SUM() devienne une fonction fenêtre, vous avez besoin de la clause OVER(). Le calcul de la fonction fenêtre est effectué par ordre croissant, car je veux m'assurer que le revenu est additionné de la date la plus ancienne à la plus récente. Vient ensuite la clause RANGE. Elle limite la fenêtre aux dates précédant la date actuelle (BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) et la date actuelle. Ce sont les lignes qui seront incluses dans le calcul du total courant.

Voici ce que vous obtenez lorsque vous exécutez le code :

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

Remarquez que lorsqu'il y a plusieurs valeurs pour une date (2021-05-02), le code inclut les deux lignes dans le calcul du total courant pour cette date. C'est pourquoi il y a 38 816,60 dans la colonne running_total pour cette date.

Exemple 2 - Calcul de la moyenne mobile

Voyons maintenant comment utiliser la clause RANGE avec des types de données date/heure - et sans l'aide de la colonne id ! Nous allons utiliser une table légèrement modifiée, qui s'appelle désormais revenue_per_shop. Elle contient les données relatives aux recettes de deux magasins. Les colonnes sont les suivantes :

  • date - La date de la recette.
  • shop - Le nom de la boutique.
  • revenue_amount - Le montant des recettes de ce magasin à cette date.

Tout d'abord, vérifions que vous comprenez bien ce qu'est une moyenne mobile. Une moyenne mobile sur deux jours comprend la journée en cours et la journée précédente. Voici quelques exemples de données pour vous montrer comment fonctionne une moyenne 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 moyenne mobile sur deux jours pour le 2021-05-01 est le revenu quotidien lui-même : 12,573.25. Cela s'explique par le fait qu'il n'y a pas d'autres lignes à inclure dans le calcul. Le calcul du 2021-05-02 comprend deux dates : (12 573,25 + 11 348,22)/2 = 11 960,74. Les autres lignes suivent la même logique à deux dates - la date actuelle et la date précédente.

Alors comment calculer la même mesure pour chaque magasin séparément ? Comme ceci :

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;

Le code sélectionne d'abord certaines colonnes du tableau. Puis vient la partie amusante. J'utilise la fonction AVG() sur la colonne revenue_amount parce que je veux le revenu moyen. Encore une fois, il s'agit d'une fonction de fenêtre, donc elle doit avoir la clause OVER(). J'utilise le PARTITION BY pour spécifier la colonne sur laquelle je veux agréger les données ; c'est la colonne shop car je veux la moyenne mobile pour chaque magasin séparément. L'opération est à nouveau ordonnée par date. Dans la clause RANGE, je spécifie simplement les lignes à inclure dans le calcul. Puisque je travaille avec des dates, je vais obtenir la date précédente en indiquant : BETWEEN INTERVAL '1' DAY PRECEDING.

Le travail avec les données de date/heure diffère selon les bases de données. Vous devrez peut-être l'écrire de cette façon dans certaines bases de données : RANGE BETWEEN 1 DAY PRECEDING AND CURRENT ROW. Le résultat devrait néanmoins être le même :

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

Moyenne mobile pour les bases de données qui ne prennent pas en charge l'utilisation de RANGE avec les types de données date/heure

Que devez-vous faire si votre base de données ne prend pas en charge l'utilisation de RANGE avec les types de données date/heure ? Il existe un moyen de "tromper" votre base de données ; en fait, il existe probablement plusieurs moyens. En voici une qui calcule la différence entre chaque date et le 2021-05-01 (c'est-à-dire la première date des données). Vous obtenez la différence sous la forme d'un nombre entier, que vous pouvez utiliser à la place de la date. Voici le code :

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;

J'ai intentionnellement inclus la colonne day_difference dans le résultat pour que vous compreniez la logique. J'ai utilisé la même différence dans la clause ORDER BY afin de pouvoir utiliser un nombre entier avec la clause RANGE. Et la moyenne mobile est la même que dans l'exemple ci-dessus ; voyez par vous-même. (Je ne montre que les cinq premières lignes pour gagner de la place).

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

Exemple 3 - Trouver la dernière valeur d'une plage de valeurs

Cette utilisation de la clause RANGE vous permet de trouver la dernière valeur dans une plage définie. Par exemple, en utilisant le tableau revenue_by_shopje peux obtenir la dernière valeur pour chaque magasin séparément. Dans ce cas, la dernière valeur correspond aux dernières données disponibles, à savoir le chiffre d'affaires du 2021-05-15. Comment obtenir ces données ?

En utilisant la clause RANGE, bien sûr :

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 fonction de fenêtre que j'ai utilisée cette fois-ci est LAST_VALUE(). Une fois encore, je l'utilise sur la colonne revenue_amount. J'ai partitionné les données par magasin, comme précédemment. Et je les ai ordonnées par date, comme précédemment. Pour obtenir la dernière valeur, j'ai utilisé RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING. N'oubliez pas que la plage par défaut avec la clause ORDER BY est RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Si vous ne la modifiez pas, vous obtiendrez un résultat erroné. Le bon résultat est le suivant :

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

Exemple 4 - Trouver le nombre d'éléments dans une plage de valeurs

Voici une autre façon amusante et utile d'utiliser la clause RANGE. Comment trouver le nombre de fois où le revenu quotidien d'un magasin se situe entre 1 000 (dollars, euros...) en dessous et au-dessus de sa valeur actuelle ?

Ce code pourrait vous aider :

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;

J'utilise la fonction de fenêtre COUNT(). Comme je ne suis pas intéressé par la séparation des revenus par magasin, il n'y a pas de PARTITION BY. Le comptage sera effectué par ordre croissant en fonction du montant du revenu. L'intervalle est défini par RANGE BETWEEN 1000 PRECEDING AND 1000 FOLLOWING.

Voici ce que le code va retourner :

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

Laissez-moi vous expliquer ce que ce résultat vous dit. Si vous prenez la première ligne, le résultat dans la colonne number_of_days est 2. Il y a deux cas où le revenu est compris entre 3 987,56 et 5 987,56. Pourquoi cette fourchette ? Le revenu pour 2021-05-13 est de 4 987,56. Donc 4 987,56 - 1 000 = 3 987,56 et 4 987,56 + 1 000 = 5 987,56. Voulez-vous vérifier le résultat ? Quelles sont les deux instances qui se situent dans cette fourchette ? Évidemment, les deux premières :

shopdaterevenue_amountnumber_of_days
Shop 22021-05-134,987.562
Shop 22021-05-115,894.123

Voulez-vous vérifier la deuxième ligne ? Elle dit qu'il y a trois instances entre 4 894,12 et 6 894,12 - ces trois-là :

shopdaterevenue_amountnumber_of_days
Shop 22021-05-134,987.562
Shop 22021-05-115,894.123
Shop 12021-05-086,874.232

La même logique s'applique au reste des données.

Exemple 5 - Trouver la valeur maximale

Cet exemple n'est pas aussi simple qu'il n'y paraît. Je ne parle pas de la valeur maximale commune. Pour la trouver, vous n'auriez pas besoin de la clause RANGE. Mais que diriez-vous de trouver la valeur maximale (ou le revenu, dans ce cas) sur cinq jours ? Ces cinq jours comprennent la date du jour, jusqu'à trois jours avant et un jour après la date du jour. Vous connaissez probablement déjà la logique après tous ces exemples d'utilisation de RANGE. Voici ma solution :

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;

J'utilise la fonction MAX() comme une fonction fenêtre. Encore une fois, je l'utilise avec la colonne revenue_amount. Il n'y a pas de PARTITION BY dans la clause OVER() car je ne suis pas intéressé par la séparation des données à quelque niveau que ce soit. La définition de la plage n'est pas très difficile : RANGE BETWEEN INTERVAL '3' DAY PRECEDING AND INTERVAL '1' DAY FOLLOWING. Cela comprendra la date actuelle, trois jours avant et un jour après. Voici le résultat :

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

Vérifions le résultat pour 2021-05-05 - marqué en rose. La plage est marquée en jaune. Pour obtenir le revenu maximum dans cette plage, SQL va comparer les valeurs : 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. Lequel est le plus élevé ? C'est 21 489,22.

Après avoir appris comment trouver la valeur maximale à l'aide de la clause RANGE, j'ai atteint le nombre maximum d'exemples prévus pour cet article. Si vous voulez d 'autres exemples de fonctions de fenêtre, vous pouvez toujours lire cet article.

RANGE a vraiment beaucoup d'utilisations, n'est-ce pas ?

Je pense que ces cinq exemples vous montrent un bon éventail des possibilités de la clause RANGE. Ce n'est pas tout ce que vous pouvez faire avec elle. Son utilisation dépend des données dont vous disposez et probablement d'un peu d'imagination. On ne parle pas souvent de cette clause, ce qui est dommage. Je conseille à tout le monde de l'apprendre. Elle pourrait vous faire gagner du temps dans la création de solutions de contournement. La clause RANGE est élégante et vraiment pas très compliquée.

Où pouvez-vous apprendre à connaître la clause RANGE ? Dans notre coursFonctions de fenêtrage , bien sûr. Vous y apprendrez non seulement la clause RANGE, mais aussi tous les aspects des fonctions de la fenêtre. Vous pouvez également utiliser le cours pour mettre en pratique vos compétences en SQL. Si vous voulez en savoir plus, cet article vous dira tout sur le cours et ce qu'il propose.