Dans la plupart des projets BI, tu vas toujours avoir besoin à un moment donner d'analyser des données par groupe (par client, par produit, par jour...), tout en gardant le même niveau de granularité. Souvent, on fait un GROUP BY puis on rejoint au détail, ou on passe par des sous-requêtes/CTE. Ça marche, mais ça peut vite devenir lourd à maintenir.

Les window fonctions sont là pour simplifier ça. Elles ne remplacent pas GROUP BY, elles le complètent. Tu gardes tes lignes, et tu ajoutes une colonne calculée autour de la ligne courante comme numéroter (ROW_NUMBER()), classer (RANK(), DENSE_RANK()), faire un cumul ou une moyenne (SUM() OVER, AVG() OVER), trouver un min/max (MIN(), MAX()), comparer avec la ligne d'avant ou d'après (LAG(), LEAD()), ou afficher la première/dernière valeur (FIRST_VALUE(), LAST_VALUE()).

La syntaxe est toujours la même, elle commence avec une fonction suivie d'un OVER(...). Dans OVER, tu peux découper tes données avec PARTITION BY , définir un ordre avec ORDER BY , et parfois préciser une fenêtre de calcul avec ROWS BETWEEN ... AND ... quand tu veux un cumul ou une moyenne ligne par ligne.

<fonction>() OVER (
  PARTITION BY ...
  ORDER BY ...
  ROWS BETWEEN ... AND ...
)

Quelle window function pour quel besoin ?

Besoin Fonction Exemple rapide
Numéroter les lignes d'un groupe ROW_NUMBER() ROW_NUMBER() OVER (PARTITION BY client ORDER BY date)
Classer avec gestion des égalités RANK() / DENSE_RANK() RANK() OVER (ORDER BY montant DESC)
Calculer un cumul / running total SUM() OVER SUM(amount) OVER (PARTITION BY client ORDER BY date)
Calculer une moyenne mobile AVG() OVER ... ROWS BETWEEN AVG(amount) OVER (ORDER BY date ROWS 2 PRECEDING)
Comparer avec ligne précédente LAG() LAG(amount) OVER (PARTITION BY client ORDER BY date)
Comparer avec ligne suivante LEAD() LEAD(amount) OVER (PARTITION BY client ORDER BY date)
Premier ou dernier d'un groupe FIRST_VALUE() / LAST_VALUE() FIRST_VALUE(amount) OVER (PARTITION BY client ORDER BY date)
Filtrer le top-N par groupe QUALIFY ROW_NUMBER() QUALIFY ROW_NUMBER() OVER (...) <= 3

Exemples

Comme dans les articles précédents, il n'y a pas mieux que des exemples concrets pour comprendre le concept et voir comment ça fonctionne.

USE DATABASE Demo_db;
USE SCHEMA demo;

--Créer une table de démo

CREATE OR REPLACE TABLE demo_orders (
  order_id     INTEGER,
  customer_id  INTEGER,
  order_date   DATE,
  amount       NUMBER(10,2),
  channel      VARCHAR
);

Insert (20 lignes) :

INSERT INTO demo_orders (order_id, customer_id, order_date, amount, channel) VALUES
  (1001, 101, DATE '2025-01-03', 120.00, 'WEB'),
  (1002, 101, DATE '2025-01-07',  80.00, 'STORE'),
  (1003, 101, DATE '2025-01-10', 200.00, 'WEB'),
  (1004, 101, DATE '2025-01-10',  50.00, 'WEB'),
  (1005, 101, DATE '2025-02-02',  90.00, 'STORE'),
  (1006, 101, DATE '2025-02-15', 300.00, 'WEB'),

  (2001, 102, DATE '2025-01-05',  60.00, 'WEB'),
  (2002, 102, DATE '2025-01-20', 400.00, 'STORE'),
  (2003, 102, DATE '2025-02-01', 150.00, 'WEB'),
  (2004, 102, DATE '2025-02-01',  75.00, 'STORE'),
  (2005, 102, DATE '2025-03-12', 220.00, 'WEB'),

  (3001, 103, DATE '2025-01-02', 500.00, 'STORE'),
  (3002, 103, DATE '2025-01-18',  40.00, 'WEB'),
  (3003, 103, DATE '2025-02-10', 120.00, 'WEB'),
  (3004, 103, DATE '2025-02-28', 120.00, 'STORE'),
  (3005, 103, DATE '2025-03-05',  90.00, 'WEB'),

  (4001, 104, DATE '2025-01-11',  30.00, 'WEB'),
  (4002, 104, DATE '2025-02-11',  35.00, 'WEB'),
  (4003, 104, DATE '2025-03-01',  25.00, 'STORE'),
  (4004, 104, DATE '2025-03-15',  60.00, 'STORE');

Voir le contenu de la table :

SELECT *
FROM demo_orders;

Scénario 1 : Numéroter les commandes d’un client (ROW_NUMBER)

SELECT
  customer_id,
  order_date,
  order_id,
  amount,
  ROW_NUMBER() OVER (
    PARTITION BY customer_id
    ORDER BY order_date, order_id
  ) AS order_n
FROM demo_orders
ORDER BY customer_id, order_date, order_id;
Scénario 1 : Numéroter les commandes d’un client (ROW_NUMBER)
Scénario 1 : Numéroter les commandes d’un client (ROW_NUMBER)

Scénario 2 : Mesurer l’évolution entre deux commandes (calculer la variation (LAG))

Concrètement dans ce use case, la fonction Lag va permettre de récupérer la valeur de la ligne précédente (du même client, dans l’ordre des dates), sans faire de self-join.

SELECT
  customer_id,
  order_date,
  order_id,
  amount,
  LAG(amount) OVER (
    PARTITION BY customer_id
    ORDER BY order_date, order_id
  ) AS prev_amount,
  amount - LAG(amount) OVER (
    PARTITION BY customer_id
    ORDER BY order_date, order_id
  ) AS diff_vs_prev
FROM demo_orders
WHERE customer_id = 101
ORDER BY order_date, order_id;
Scénario 2 : Mesurer l’évolution entre deux commandes (calculer la variation (LAG))
Scénario 2 : Mesurer l’évolution entre deux commandes (calculer la variation (LAG))

Scénario 3 : Suivre le cumul des dépenses client après chaque commande (calculer un cumul)

Objectif : afficher, pour chaque commande, le total dépensé par le client jusqu’à cette commande.

SELECT
  customer_id,
  order_date,
  order_id,
  amount,

  -- cumul "par date" (si plusieurs lignes le même jour, ça peut regrouper)
  SUM(amount) OVER (
    PARTITION BY customer_id
    ORDER BY order_date
  ) AS cumul_by_date,

  -- cumul "ligne par ligne" 
  SUM(amount) OVER (
    PARTITION BY customer_id
    ORDER BY order_date, order_id
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS cumul_by_rows

FROM demo_orders
WHERE customer_id = 101
ORDER BY order_date, order_id;
Scénario 3 : Suivre le cumul des dépenses client après chaque commande (calculer un cumul)
Scénario 3 : Suivre le cumul des dépenses client après chaque commande (calculer un cumul)

Scénario 4 : Top 3 commandes les plus chères par client

💡
QUALIFY

QUALIFY est l'équivalent de WHERE pour les window functions. Au lieu d'enrouler ta requête dans un CTE pour filtrer sur le résultat d'un ROW_NUMBER(), tu peux filtrer directement avec QUALIFY. Disponible sur Snowflake, BigQuery, Databricks et DuckDB. Ne fonctionne pas sur PostgreSQL ni MySQL.

Avant (avec CTE) :WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY amount DESC) AS rn
FROM demo_orders
)
SELECT * FROM ranked WHERE rn <= 3;


Après (avec QUALIFY) :SELECT *
FROM demo_orders
QUALIFY ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY amount DESC) <= 3;


Plus compact, plus lisible, même résultat.
SELECT
  customer_id,
  order_id,
  order_date,
  amount
FROM demo_orders
QUALIFY ROW_NUMBER() OVER (
  PARTITION BY customer_id
  ORDER BY amount DESC, order_date DESC, order_id DESC
) <= 3
ORDER BY customer_id, amount DESC, order_id DESC;
Scénario 4 : Top 3 commandes les plus chères par client
Scénario 4 : Top 3 commandes les plus chères par client
💡
Alternative avec LATERAL JOIN : sur Snowflake, tu peux aussi obtenir le top-N par groupe avec JOIN LATERAL, qui s'exprime parfois plus naturellement. Quand N est petit (1 à 5), LATERAL peut même être plus rapide grâce au early termination. Voir LATERAL JOIN et CROSS APPLY : exécuter une sous-requête par ligne en SQL.

Scénario 5 : Part et poids de chaque commande dans le total du client

SELECT
  customer_id,
  order_date,
  order_id,
  amount,
  SUM(amount) OVER (PARTITION BY customer_id) AS total_customer,
  ROUND(100 * amount / SUM(amount) OVER (PARTITION BY customer_id), 2) AS pct_of_customer
FROM demo_orders
ORDER BY order_date, order_id;
Scénario 5 : Part ou le point de chaque commande dans le total client
Scénario 5 : Part ou le point de chaque commande dans le total client

Scénario 6 : Moyenne des 3 dernières commandes

SELECT
  customer_id,
  order_date,
  order_id,
  amount,
  ROUND(
    AVG(amount) OVER (
      PARTITION BY customer_id
      ORDER BY order_date, order_id
      ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ), 2
  ) AS avg_last_3_orders
FROM demo_orders
ORDER BY order_date, order_id;
Scénario 6 : Moyenne des 3 dernières commandes
Scénario 6 : Moyenne des 3 dernières commandes

Scénario 7 : Top 3 commandes par canal

SELECT
  channel,
  order_id,
  customer_id,
  order_date,
  amount
FROM demo_orders
QUALIFY ROW_NUMBER() OVER (
  PARTITION BY channel
  ORDER BY amount DESC, order_date DESC, order_id DESC
) <= 3
ORDER BY channel;
Scénario 7 : Top 3 commandes par canal
Scénario 7 : Top 3 commandes par canal

Scénario 8 : Calculer l'écart en jours entre commandes consécutives (LAG + DATEDIFF)


SELECT
  customer_id,
  order_id,
  order_date,
  DATEDIFF(
    'day',
    LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date, order_id),
    order_date
  ) AS days_since_prev
FROM demo_orders
//QUALIFY days_since_prev IS NOT NULL
ORDER BY customer_id, order_date, order_id;
Scénario 8 : Premier achat et dernier achat du client sur chaque ligne (MIN & MAX)
Scénario 8 : Premier achat et dernier achat du client sur chaque ligne (MIN & MAX)
💡
La fonction DATEDIFF calcule l'écart en jours, semaines, mois ou heures entre deux dates. Pour creuser toutes les fonctions de date, voir Manipuler les dates en SQL Snowflake : DATE_TRUNC, DATEADD, DATEDIFF.

Aller plus loin : pratiquer les window functions

Les window functions, comme la plupart du SQL avancé, ne s'apprennent vraiment qu'en pratiquant. Sur DataCertification.fr, le module SQL Lab propose 240+ exercices interactifs avec une partie dédiée aux window functions (ROW_NUMBER, LAG, cumul, top-N avec QUALIFY).

👉 Pratiquer les window functions sur DataCertification.fr

Pour aller plus loin sur Snowflake, j'ai regroupé tous mes articles dans un parcours complet :

👉 Accéder à la Formation Snowflake

Tu veux que je t'accompagne sur ton projet data (modélisation, performance SQL, Snowflake, dbt) ?

👉 Réserver un appel de 30 minutes

Questions

C'est quoi une window function en SQL ?

Une window function est une fonction SQL qui calcule une valeur sur un sous-ensemble de lignes (la fenêtre) tout en gardant la granularité d'origine. Contrairement à GROUP BY qui agrège et réduit le nombre de lignes, une window function ajoute une colonne calculée à chaque ligne. La syntaxe est fonction() OVER (PARTITION BY ... ORDER BY ...).

Quelle différence entre OVER, PARTITION BY et ORDER BY dans une window function ?

OVER déclare qu'il s'agit d'une window function. PARTITION BY définit le groupe sur lequel calculer (équivalent du GROUP BY mais sans réduire les lignes). ORDER BY définit l'ordre à l'intérieur de chaque groupe, ce qui est obligatoire pour ROW_NUMBER, LAG, LEAD et les calculs cumulés.

ROW_NUMBER, RANK ou DENSE_RANK : quelle différence ?

ROW_NUMBER attribue un numéro unique à chaque ligne dans l'ordre (1, 2, 3, 4...). RANK attribue le même rang aux égalités et saute des rangs après (1, 2, 2, 4...). DENSE_RANK attribue le même rang aux égalités sans sauter (1, 2, 2, 3...). Pour numéroter sans ambiguïté, ROW_NUMBER. Pour classer avec gestion des égalités, RANK ou DENSE_RANK.

Comment faire un top N par groupe en SQL Snowflake ?

Deux approches en Snowflake : (1) avec QUALIFY directement : SELECT * FROM commandes QUALIFY ROW_NUMBER() OVER (PARTITION BY client ORDER BY date DESC) <= 3. (2) avec un CTE qui calcule ROW_NUMBER puis filtre. QUALIFY est plus compact et est aussi disponible sur BigQuery et Databricks. Sur PostgreSQL ou MySQL, il faut passer par un CTE.

À quoi sert QUALIFY en SQL Snowflake ?

QUALIFY est l'équivalent de WHERE mais pour les window functions. Il permet de filtrer directement sur le résultat d'une window function sans passer par un CTE intermédiaire. C'est une clause spécifique à Snowflake, BigQuery, Databricks et DuckDB. Très utile pour le top-N par groupe : QUALIFY ROW_NUMBER() OVER (...) <= 3.

Comment calculer un cumul (running total) en SQL ?

Utilise SUM(colonne) OVER (PARTITION BY groupe ORDER BY ordre). Pour un cumul ligne par ligne strict, ajoute ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Sans cette clause, Snowflake calcule par défaut en mode RANGE qui peut regrouper les lignes de même ordre. La clause ROWS est explicite et donne un cumul ligne par ligne déterministe.

Quelle différence entre LAG et LEAD ?

LAG renvoie la valeur de la ligne précédente dans l'ordre du window. LEAD renvoie la valeur de la ligne suivante. Les deux prennent un offset optionnel (par défaut 1). LAG(montant) OVER (PARTITION BY client ORDER BY date) compare avec la commande précédente. C'est pratique pour calculer une variation, un écart en jours, ou détecter un changement.

Quelle différence entre ROWS BETWEEN et RANGE BETWEEN ?

ROWS BETWEEN définit un cadre par nombre physique de lignes (2 PRECEDING = exactement 2 lignes avant la ligne courante). RANGE BETWEEN définit le cadre par valeurs de l'ORDER BY (2 PRECEDING = toutes les lignes dont la valeur d'ordre est dans une plage de 2). RANGE peut donc embarquer plusieurs lignes si plusieurs ont la même valeur d'ordre.

Quelle différence entre GROUP BY et window function ?

GROUP BY agrège les lignes et réduit le nombre de résultats (une ligne par groupe). Window function calcule sur un groupe (PARTITION BY) mais conserve toutes les lignes d'origine en ajoutant une colonne calculée. GROUP BY répond à 'quel est le total par client'. Window function répond à 'pour chaque commande, quel est le total cumulé du client'.

Comment calculer une moyenne mobile (rolling average) en SQL ?

Utilise AVG(colonne) OVER (PARTITION BY groupe ORDER BY ordre ROWS BETWEEN N PRECEDING AND CURRENT ROW). Par exemple, pour la moyenne des 3 dernières commandes : AVG(amount) OVER (PARTITION BY customer_id ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW). Le 2 PRECEDING + CURRENT ROW couvre bien 3 lignes.