Apprenez en lisant dans l'ordre

Fonctions de fenêtrage ② — ORDER BY et cadres (ROWS / RANGE)

Visualise les totaux cumulés avec OVER (... ORDER BY ...), les sommes mobiles avec ROWS BETWEEN 2 PRECEDING, et la différence ROWS vs RANGE sur les lignes ex æquo — le tout exploré avec des données de ventes et des schémas.

Fenêtres avec ORDER BY — produire un total cumulé

Ajoute ORDER BY dans OVER et la cible de l'agrégation passe à « du début de la fenêtre jusqu'à la ligne courante » — la valeur grandit ligne par ligne, produisant un total cumulé (la somme depuis le début jusqu'à la ligne courante).

La forme est SUM(amount) OVER (PARTITION BY emp_id ORDER BY sale_date).

PARTITION BY emp_id divise la fenêtre par employé, ORDER BY sale_date organise les ventes de chaque employé par date, puis le total cumulé est calculé par employé.

Le schéma ci-dessous trace la croissance du total cumulé pour les 5 ventes d'emp_id=29 dans l'ordre des dates.

Ajouter ORDER BY produit un total cumulé
sale_dateamountrunning_total(SUM OVER ORDER BY date)01-07310000310000(= 310000)02-14295000605000(= 310 + 295)03-21280000885000(= 605 + 280)04-052650001150000(= 885 + 265)06-233250001475000(= 1150 + 325)dernière ligne = total des ventes de l'employé+295000+280000+265000+325000
En triant les 5 ventes d'emp_id=29 par sale_date, running_total vaut « total cumulé précédent + amount courant ». Il grandit 310000 → 605000 → 885000 → 1150000 → 1475000, la dernière ligne correspondant au total des ventes de l'employé.
-- Sans ORDER BY : chaque ligne reçoit le total inchangé de l'employé
SELECT emp_id, sale_date, amount,
  SUM(amount) OVER (PARTITION BY emp_id) AS emp_total
FROM sales
ORDER BY emp_id, sale_date;

-- Avec ORDER BY : total cumulé par employé du début jusqu'à la ligne courante
SELECT emp_id, sale_date, amount,
  SUM(amount) OVER (PARTITION BY emp_id ORDER BY sale_date) AS running_total
FROM sales
ORDER BY emp_id, sale_date;

Imagine que tu veux un total cumulé des ventes par employé, trié par date. (Si ta requête s'exécute correctement, une explication apparaîtra.)

① Sélectionne emp_id, sale_date et amount depuis sales.

② Calcule SUM(amount) sur une fenêtre partitionnée par employé et triée par date, et ajoute-le comme 4ᵉ colonne avec l'alias running_total.

③ Le correcteur vérifie l'ordre des lignes, donc termine par ORDER BY emp_id, sale_date.

Éditeur SQL

Exécutez une requête pour voir les résultats

Le cadre — quelles lignes sont réellement agrégées dans la fenêtre

Lorsque tu ajoutes ORDER BY à une fonction de fenêtrage, la plage de lignes que chaque ligne agrège réellement s'appelle le cadre.

Cadre — lignes réellement agrégées dans la fenêtre
fenêtre (5 lignes)cadre (3 lignes agrégées)fenêtre(PARTITION BY emp_id= lignes d'un employé)cadre(lignes pour le calcul de la ligne courante)ligne 1ligne 2ligne 3 ← ligne couranteligne 4ligne 5ligne 1ligne 2ligne 3 (courante)toutes les lignes d'un employélignes agrégées pour calculer la ligne 3(défaut : début jusqu'à la ligne courante)
Dans une fenêtre (par exemple les 5 lignes d'emp_id=14), le cadre décide « d'ici à ici, additionne ces lignes » pour la ligne courante. Change le cadre et la signification du calcul change — total cumulé, somme mobile, moyenne des voisins, etc.

Si tu n'écris pas le cadre explicitement, la valeur par défaut est RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW (du début de la fenêtre jusqu'à la ligne courante).

C'est exactement pour cela qu'ajouter ORDER BY dans l'exercice précédent te donnait un total cumulé — ce cadre par défaut fait le travail.

-- A : cadre omis (défaut = RANGE UNBOUNDED PRECEDING ... CURRENT ROW)
SELECT emp_id, sale_date, amount,
  SUM(amount) OVER (PARTITION BY emp_id ORDER BY sale_date) AS run_default
FROM sales WHERE emp_id = 14
ORDER BY sale_date;

-- B : le cadre par défaut écrit explicitement (même résultat que A)
SELECT emp_id, sale_date, amount,
  SUM(amount) OVER (
    PARTITION BY emp_id ORDER BY sale_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS run_explicit
FROM sales WHERE emp_id = 14
ORDER BY sale_date;

Les cadres se construisent à partir de ces 5 mots-clés, utilisés pour dire « depuis où » et « jusqu'où » par rapport à la ligne courante.

Mot-cléSignification
UNBOUNDED PRECEDINGTout en arrière jusqu'au début de la fenêtre
n PRECEDINGn lignes avant la ligne courante
CURRENT ROWLa ligne courante elle-même
n FOLLOWINGn lignes après la ligne courante
UNBOUNDED FOLLOWINGTout en avant jusqu'à la fin de la fenêtre
Défaut (avec ORDER BY)RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

Confirmons le comportement de total cumulé en écrivant le cadre explicitement.

① Sélectionne emp_id, sale_date et amount depuis sales.

② Sur une fenêtre partitionnée par employé et triée par sale_date, calcule SUM(amount) avec le cadre explicite ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, et ajoute-le comme 4ᵉ colonne avec l'alias running_total.

③ Le correcteur vérifie l'ordre des lignes, donc termine par ORDER BY emp_id, sale_date.

Éditeur SQL

Exécutez une requête pour voir les résultats

ROWS BETWEEN n PRECEDING — sommes mobiles et moyennes mobiles

Configure le cadre sur ROWS BETWEEN n PRECEDING AND CURRENT ROW et l'agrégation se restreint aux « n dernières lignes + la ligne courante ».

ROWS BETWEEN 2 PRECEDING AND CURRENT ROW couvre les 3 lignes les plus récentes (en incluant la courante), donc SUM devient une somme mobile et AVG devient une moyenne mobile.

C'est la forme classique pour lisser une tendance récente.

Près du début de la fenêtre, le cadre a moins de lignes à puiser (la ligne 1 a 1 ligne, la ligne 2 a 2 lignes, à partir de la ligne 3 il y a 3 lignes).

Le schéma ci-dessous parcourt les 5 ventes d'emp_id=14 ligne par ligne, en montrant ce qui se retrouve dans le cadre à chaque « ligne courante ».

ROWS BETWEEN 2 PRECEDING — cadre glissant de 3 lignes
ligne courante (amount)contenu du cadre (3 lignes récentes)somme mobile (SUM)ligne 1490000[490000] juste 1 ligne490000ligne 2100000[490000, 100000] 2 lignes590000ligne 385000[490000, 100000, 85000] 3 lignes675000ligne 470000[100000, 85000, 70000] 3 lignes(490000 sort)255000ligne 5505000[85000, 70000, 505000] 3 lignes(100000 sort)660000le cadre « 3 lignes récentes » glisse
Pour les 5 ventes d'emp_id=14 (amounts 490000 / 100000 / 85000 / 70000 / 505000), le cadre glisse avec la ligne courante : « les 2 dernières lignes + la ligne courante ». Près du début il n'y a pas assez de lignes précédentes, le cadre fait donc 1 ou 2 lignes de large. Suis les flèches : ligne courante → cadre → somme mobile.
-- Moyenne mobile des 3 dernières ventes (2 lignes préc. + courante)
SELECT emp_id, sale_date, amount,
  AVG(amount) OVER (
    PARTITION BY emp_id ORDER BY sale_date
    ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
  ) AS moving_avg3
FROM sales WHERE emp_id = 2
ORDER BY sale_date;

-- Somme mobile des 2 dernières ventes (1 ligne préc. + courante) utilise la même forme
SELECT emp_id, sale_date, amount,
  SUM(amount) OVER (
    PARTITION BY emp_id ORDER BY sale_date
    ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
  ) AS moving_sum2
FROM sales WHERE emp_id = 2
ORDER BY sale_date;

Imagine que tu veux une somme mobile par employé sur les 3 ventes les plus récentes (les 2 dernières + la courante), triée par date.

① Sélectionne emp_id, sale_date et amount depuis sales.

② Sur une fenêtre partitionnée par employé et triée par sale_date, calcule SUM(amount) avec le cadre ROWS BETWEEN 2 PRECEDING AND CURRENT ROW, et ajoute-le comme 4ᵉ colonne avec l'alias moving_sum3.

③ Le correcteur vérifie l'ordre des lignes, donc termine par ORDER BY emp_id, sale_date.

Éditeur SQL

Exécutez une requête pour voir les résultats

ROWS vs RANGE — comment les lignes ex æquo sont traitées

Les cadres existent en deux modes de comptage : ROWS et RANGE.

ROWS compte les lignes physiques (« les 2 lignes précédentes » signifie exactement 2 lignes). Même avec des valeurs ex æquo, chaque ligne est comptée séparément, donc le total cumulé grandit ligne par ligne.

RANGE regroupe les lignes ayant la même valeur d'ORDER BY (ex æquo) dans un seul bloc. Les lignes ex æquo partagent un cadre et finissent avec le même total cumulé.

Les deux schémas ci-dessous montrent comment ROWS et RANGE traitent chacun les 3 lignes où amount = 100000.

ROWS — ex æquo comptés un à un, total grandit
ligne courantecadre ROWS (début → ligne courante)rows_runsale_id 21100000[21]1 ligne100000sale_id 24100000[21, 24]2 lignes200000sale_id 37100000[21, 24, 37]3 lignes300000
Pour les 3 lignes avec amount = 100000 (triées par amount), le cadre ROWS compte les lignes physiques jusqu'à la ligne courante. La ligne 1 a [21] (1 ligne), la ligne 2 a [21,24] (2 lignes), la ligne 3 a [21,24,37] (3 lignes) — le total cumulé passe par 100000 → 200000 → 300000.
RANGE — les lignes ex æquo partagent un cadre et un total
3 lignes ex æquo forment un seul cadreligne courantecadre RANGE (ex æquo regroupés)range_runsale_id 21100000[21, 24, 37]3 lignes ex æquo300000sale_id 24100000[21, 24, 37]3 lignes ex æquo300000sale_id 37100000[21, 24, 37]3 lignes ex æquo300000
RANGE regroupe les lignes avec la même valeur d'ORDER BY (ex æquo). Les 3 lignes avec amount = 100000 atterrissent toutes dans le même cadre [21, 24, 37], et le total cumulé est 300000 sur chaque ligne.
-- ORDER BY amount a 3 lignes ex æquo à 100000, donc ROWS et RANGE divergent
SELECT sale_id, amount,
  SUM(amount) OVER (ORDER BY amount ROWS  UNBOUNDED PRECEDING) AS rows_run,
  SUM(amount) OVER (ORDER BY amount RANGE UNBOUNDED PRECEDING) AS range_run
FROM sales
WHERE amount = 100000
ORDER BY sale_id;
-- ROWS compte une ligne à la fois ; RANGE regroupe les 3 lignes 100000 ex æquo dans un seul cadre

Imagine que tu veux voir, dans un seul résultat, comment un total cumulé change entre ROWS et RANGE lorsque plusieurs lignes partagent la même valeur. Il y a 3 ventes où amount = 100000.

① Sélectionne sale_id et amount depuis sales.

② Sur une fenêtre triée par amount, ajoute SUM(amount) avec le cadre ROWS UNBOUNDED PRECEDING avec l'alias rows_run, et un autre SUM(amount) avec le cadre RANGE UNBOUNDED PRECEDING avec l'alias range_run.

③ Filtre uniquement les lignes où amount = 100000.

④ Le correcteur vérifie l'ordre des lignes, donc termine par ORDER BY sale_id.

Éditeur SQL

Exécutez une requête pour voir les résultats
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Lorsque tu ajoutes ORDER BY dans OVER, comme SUM(amount) OVER (PARTITION BY emp_id ORDER BY sale_date), quelle valeur se retrouve sur chaque ligne ?

Question 2Lorsqu'une fonction de fenêtrage a ORDER BY et que tu n'écris pas le cadre explicitement, quel est le cadre par défaut ?

Question 3Quelle est la différence entre les cadres ROWS et RANGE ?