GROUP BY et HAVING en SQL : le guide complet avec exemples

Maîtrisez le regroupement et le filtrage des agrégats en SQL grâce à GROUP BY et HAVING

Matière
SQL / Bases de données
Niveau
Intermédiaire
Clauses couvertes
GROUP BY, HAVING, WHERE, fonctions d’agrégation
Compatibilité
MySQL, PostgreSQL, SQL standard
La clause GROUP BY est l’une des fonctionnalités les plus puissantes de SQL. Elle permet de regrouper des lignes ayant les mêmes valeurs et d’appliquer des fonctions d’agrégation (COUNT, SUM, AVG…) sur chaque groupe. La clause HAVING complète GROUP BY en permettant de filtrer les groupes après l’agrégation — là où WHERE filtre les lignes individuelles avant. Ensemble, ces deux clauses forment le cœur de toute analyse de données en SQL : elles permettent de produire des rapports, des statistiques et des tableaux de bord directement depuis une base de données relationnelle. Ce cours explique GROUP BY et HAVING avec des exemples concrets, des pièges courants, et un quiz pour vérifier vos connaissances.

1. Le principe du GROUP BY

GROUP BY regroupe les lignes qui ont la même valeur dans une ou plusieurs colonnes, et permet d’appliquer un calcul (somme, moyenne, décompte…) sur chaque groupe. Sans GROUP BY, les fonctions d’agrégation s’appliquent à toute la table.

Prenons une table commandes avec des colonnes id, client, produit, montant et date_achat.

Sans GROUP BY — le total global :

SELECT SUM(montant) AS total_global
FROM commandes;

Résultat : une seule ligne avec le total de toutes les commandes.

Avec GROUP BY — le total par client :

SELECT client, SUM(montant) AS total_depenses
FROM commandes
GROUP BY client;

Résultat : une ligne par client, avec le total de ses commandes. C’est la force du GROUP BY : il transforme N lignes en un résumé par groupe. Concrètement, si la table contient 10 000 commandes réparties sur 300 clients, la requête retourne exactement 300 lignes — une par client.

Syntaxe générale :

SELECT colonne_groupe, fonction_agregation(colonne)
FROM table
WHERE conditions_sur_les_lignes
GROUP BY colonne_groupe
HAVING conditions_sur_les_groupes
ORDER BY colonne;

Pour approfondir les bases du langage SQL avant d’aborder GROUP BY, consultez notre cours SQL complet pour débutants. Si vous travaillez spécifiquement avec MySQL, notre cours MySQL couvre les spécificités de ce SGBD.

2. Les fonctions d’agrégation

Les fonctions d’agrégation calculent une valeur unique à partir d’un ensemble de lignes. Elles sont presque toujours utilisées avec GROUP BY.

FonctionDescriptionExemple
COUNT(*)Nombre de lignes dans le groupeCOUNT(*) — compte toutes les lignes, y compris celles avec NULL
COUNT(colonne)Nombre de valeurs non NULLCOUNT(email) — ignore les lignes où email est NULL
COUNT(DISTINCT col)Nombre de valeurs uniques non NULLCOUNT(DISTINCT ville)
SUM(colonne)Somme des valeursSUM(montant)
AVG(colonne)Moyenne des valeursAVG(note) — ignore les NULL
MIN(colonne)Plus petite valeurMIN(prix)
MAX(colonne)Plus grande valeurMAX(date_inscription)
Attention : AVG(), SUM(), MIN() et MAX() ignorent les valeurs NULL. Si une colonne contient 5 valeurs dont 2 NULL, AVG() fait la moyenne sur les 3 valeurs non NULL, pas sur 5. Ce comportement peut conduire à des résultats inattendus si vous ne gérez pas les NULL en amont.

Combiner plusieurs fonctions d’agrégation dans une même requête :

-- Statistiques complètes par catégorie de produit
SELECT 
    categorie,
    COUNT(*)            AS nb_produits,
    MIN(prix)           AS prix_min,
    MAX(prix)           AS prix_max,
    AVG(prix)           AS prix_moyen,
    SUM(stock)          AS stock_total
FROM produits
GROUP BY categorie
ORDER BY nb_produits DESC;

3. Exemples concrets de GROUP BY

Compter le nombre de commandes par client :

SELECT client, COUNT(*) AS nb_commandes
FROM commandes
GROUP BY client;

Calculer le chiffre d’affaires par produit :

SELECT produit, SUM(montant) AS ca_total
FROM commandes
GROUP BY produit
ORDER BY ca_total DESC;

Trouver la commande la plus chère par client :

SELECT client, MAX(montant) AS commande_max
FROM commandes
GROUP BY client;

Calculer la note moyenne par matière :

SELECT matiere, AVG(note) AS moyenne, COUNT(*) AS nb_eleves
FROM notes
GROUP BY matiere
ORDER BY moyenne DESC;

Nombre de produits distincts achetés par client :

SELECT client, COUNT(DISTINCT produit) AS nb_produits_differents
FROM commandes
GROUP BY client;

Répartition des utilisateurs par pays :

-- Utile pour des rapports géographiques
SELECT pays, COUNT(*) AS nb_utilisateurs,
       ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 2) AS pourcentage
FROM utilisateurs
GROUP BY pays
ORDER BY nb_utilisateurs DESC;

4. GROUP BY sur plusieurs colonnes

On peut grouper par plusieurs colonnes pour obtenir des sous-groupes plus fins. Le GROUP BY crée alors un groupe pour chaque combinaison unique de valeurs.

-- Chiffre d'affaires par client ET par produit
SELECT client, produit, SUM(montant) AS total
FROM commandes
GROUP BY client, produit
ORDER BY client, total DESC;

Si on a 3 clients et 4 produits, on aura au maximum 12 groupes (3 × 4), un pour chaque combinaison client/produit effectivement présente dans la table.

Exemple avec une date :

-- Ventes par mois et par catégorie
SELECT 
    YEAR(date_achat) AS annee,
    MONTH(date_achat) AS mois,
    categorie,
    SUM(montant) AS ca
FROM ventes
GROUP BY YEAR(date_achat), MONTH(date_achat), categorie
ORDER BY annee, mois;

Exemple : répartition des commandes par statut et par année :

SELECT 
    YEAR(date_achat) AS annee,
    statut,
    COUNT(*) AS nb_commandes,
    SUM(montant) AS total
FROM commandes
GROUP BY YEAR(date_achat), statut
ORDER BY annee DESC, nb_commandes DESC;
Règle fondamentale : dans le SELECT, chaque colonne qui n’est pas dans une fonction d’agrégation doit apparaître dans le GROUP BY. Sinon, le SGBD ne sait pas quelle valeur afficher. MySQL est permissif par défaut (mode ONLY_FULL_GROUP_BY désactivé), mais PostgreSQL refusera la requête avec une erreur explicite. En SQL standard, la règle stricte s’applique toujours.

5. HAVING : filtrer après l’agrégation

HAVING filtre les groupes après que GROUP BY a créé les agrégations. C’est l’équivalent du WHERE, mais pour les résultats agrégés.

Clients ayant passé plus de 5 commandes :

SELECT client, COUNT(*) AS nb_commandes
FROM commandes
GROUP BY client
HAVING COUNT(*) > 5;

Produits dont le CA total dépasse 10 000 € :

SELECT produit, SUM(montant) AS ca_total
FROM commandes
GROUP BY produit
HAVING SUM(montant) > 10000
ORDER BY ca_total DESC;

Matières où la moyenne est inférieure à 10 :

SELECT matiere, AVG(note) AS moyenne
FROM notes
GROUP BY matiere
HAVING AVG(note) < 10;

Départements ayant entre 5 et 20 employés avec un salaire moyen > 3000 :

SELECT departement, 
       COUNT(*) AS nb_employes, 
       AVG(salaire) AS salaire_moyen
FROM employes
GROUP BY departement
HAVING COUNT(*) BETWEEN 5 AND 20
  AND AVG(salaire) > 3000;

Clients dont le panier moyen dépasse 150 € et qui ont commandé au moins 3 fois :

SELECT client,
       COUNT(*) AS nb_commandes,
       AVG(montant) AS panier_moyen,
       SUM(montant) AS total
FROM commandes
GROUP BY client
HAVING COUNT(*) >= 3
  AND AVG(montant) > 150
ORDER BY total DESC;
Peut-on utiliser un alias dans HAVING ? En MySQL oui (HAVING ca_total > 10000), mais pas en PostgreSQL ni en SQL standard. Pour être portable, répétez l'expression : HAVING SUM(montant) > 10000.

6. WHERE vs HAVING : la différence clé

C'est la question d'entretien SQL la plus fréquente. La différence est simple mais fondamentale :

WHEREHAVING
Filtre quoi ?Les lignes individuellesLes groupes (résultats agrégés)
Quand ?Avant le GROUP BYAprès le GROUP BY
Fonctions d'agrégation ?❌ Non✅ Oui
ExempleWHERE montant > 100HAVING SUM(montant) > 1000
Impact performanceRéduit les données avant regroupement ✅Filtre après calcul (moins efficace)

Exemple combinant WHERE et HAVING :

-- Clients ayant dépensé plus de 500€ en 2024
-- (en excluant les commandes annulées)
SELECT client, SUM(montant) AS total_2024
FROM commandes
WHERE statut != 'annulée'          -- filtre les lignes AVANT le regroupement
  AND YEAR(date_achat) = 2024
GROUP BY client
HAVING SUM(montant) > 500          -- filtre les groupes APRÈS le regroupement
ORDER BY total_2024 DESC;

Le WHERE a exclu les commandes annulées et celles hors 2024 avant le regroupement. Puis le HAVING n'a gardé que les clients dont le total (des commandes restantes) dépasse 500 €.

Autre exemple concret — rapport sur les vendeurs actifs :

-- Vendeurs ayant réalisé plus de 20 ventes de produits "premium"
-- avec un CA mensuel supérieur à 10 000€
SELECT vendeur_id,
       MONTH(date_vente) AS mois,
       COUNT(*) AS nb_ventes,
       SUM(montant) AS ca_mensuel
FROM ventes
WHERE categorie = 'premium'        -- WHERE filtre la catégorie AVANT le GROUP BY
  AND YEAR(date_vente) = 2024
GROUP BY vendeur_id, MONTH(date_vente)
HAVING COUNT(*) > 20               -- HAVING filtre les groupes résultants
  AND SUM(montant) > 10000
ORDER BY ca_mensuel DESC;
Piège classique : écrire WHERE COUNT(*) > 5 provoque une erreur. Les fonctions d'agrégation ne sont jamais autorisées dans WHERE — il faut utiliser HAVING.

7. Ordre d'exécution d'une requête SQL

L'ordre dans lequel vous écrivez les clauses SQL n'est pas l'ordre dans lequel le moteur les exécute. Comprendre cet ordre est essentiel pour maîtriser GROUP BY et HAVING.

ÉtapeClauseRôle
1FROM / JOINDéterminer les tables sources
2WHEREFiltrer les lignes individuelles
3GROUP BYRegrouper les lignes
4HAVINGFiltrer les groupes
5SELECTChoisir les colonnes et calculer les expressions
6DISTINCTÉliminer les doublons
7ORDER BYTrier les résultats
8LIMIT / OFFSETLimiter le nombre de lignes
Conséquences importantes :

  1. On ne peut pas utiliser un alias défini dans SELECT dans le WHERE (car WHERE est exécuté avant SELECT).
  2. On ne peut pas utiliser de fonction d'agrégation dans WHERE (car WHERE est exécuté avant GROUP BY).
  3. HAVING peut utiliser des fonctions d'agrégation car il est exécuté après GROUP BY.
  4. ORDER BY est exécuté en dernier — il peut utiliser les alias du SELECT.

Cet ordre d'exécution est directement lié à la façon dont les jointures SQL interagissent avec les agrégations : les jointures (FROM/JOIN) sont résolues en premier, avant tout filtrage.

8. GROUP BY avec des jointures

GROUP BY est souvent combiné avec des jointures pour agréger des données provenant de plusieurs tables.

Nombre de commandes par client (avec le nom du client) :

SELECT c.nom, c.email, COUNT(o.id) AS nb_commandes
FROM clients c
LEFT JOIN commandes o ON c.id = o.client_id
GROUP BY c.id, c.nom, c.email
ORDER BY nb_commandes DESC;

On utilise LEFT JOIN pour inclure les clients qui n'ont passé aucune commande (ils auront nb_commandes = 0).

Chiffre d'affaires par catégorie de produit :

SELECT cat.nom AS categorie,
       COUNT(DISTINCT o.id) AS nb_commandes,
       SUM(li.quantite * li.prix_unitaire) AS ca_total
FROM categories cat
JOIN produits p ON cat.id = p.categorie_id
JOIN lignes_commande li ON p.id = li.produit_id
JOIN commandes o ON li.commande_id = o.id
WHERE o.statut = 'livree'
GROUP BY cat.id, cat.nom
HAVING SUM(li.quantite * li.prix_unitaire) > 5000
ORDER BY ca_total DESC;

Moyenne des notes par élève avec le nom de l'élève :

SELECT e.nom, e.prenom,
       COUNT(n.id) AS nb_notes,
       AVG(n.valeur) AS moyenne_generale,
       MIN(n.valeur) AS note_min,
       MAX(n.valeur) AS note_max
FROM eleves e
LEFT JOIN notes n ON e.id = n.eleve_id
GROUP BY e.id, e.nom, e.prenom
HAVING COUNT(n.id) > 0
ORDER BY moyenne_generale DESC;
Attention au GROUP BY avec JOIN : si un client a 3 commandes et qu'on fait un JOIN avec une table adresses (2 adresses par client), on obtiendrait 6 lignes, et COUNT(*) vaudrait 6 au lieu de 3. Utilisez COUNT(DISTINCT commande.id) pour éviter ce piège.

9. GROUP BY ROLLUP et CUBE

Au-delà du GROUP BY classique, SQL propose des extensions pour générer des sous-totaux et totaux automatiques : ROLLUP et CUBE. Ces extensions sont disponibles en MySQL, PostgreSQL et la plupart des SGBD modernes.

ROLLUP : sous-totaux hiérarchiques

ROLLUP génère des sous-totaux pour chaque niveau de la hiérarchie, plus un total général.

-- CA par pays, puis par ville, avec sous-totaux par pays et total général
SELECT 
    pays,
    ville,
    SUM(montant) AS ca
FROM ventes
GROUP BY ROLLUP(pays, ville)
ORDER BY pays, ville;

Résultat : on obtient les CA par ville, puis une ligne de sous-total par pays (ville = NULL), puis une ligne de total général (pays = NULL et ville = NULL).

CUBE : toutes les combinaisons de sous-totaux

-- CUBE génère des sous-totaux pour TOUTES les combinaisons de colonnes
SELECT 
    annee,
    trimestre,
    region,
    SUM(ca) AS total_ca
FROM ventes
GROUP BY CUBE(annee, trimestre, region);

CUBE est utile pour les rapports multidimensionnels (tableaux croisés dynamiques en SQL). Avec 3 colonnes, il génère 2³ = 8 niveaux d'agrégation différents.

Identifier les lignes de sous-totaux : utilisez la fonction GROUPING(colonne) qui retourne 1 si la colonne est agrégée (ligne de sous-total) et 0 sinon. Cela permet de distinguer un NULL réel d'un NULL de sous-total introduit par ROLLUP.
SELECT 
    CASE WHEN GROUPING(pays) = 1 THEN 'TOTAL GÉNÉRAL' ELSE pays END AS pays,
    CASE WHEN GROUPING(ville) = 1 THEN 'Sous-total' ELSE ville END AS ville,
    SUM(montant) AS ca
FROM ventes
GROUP BY ROLLUP(pays, ville);

10. Pièges courants et bonnes pratiques

❌ Piège 1 : colonnes manquantes dans le GROUP BY

-- ❌ ERREUR en PostgreSQL (correct en MySQL mode permissif)
SELECT client, email, COUNT(*)
FROM commandes
GROUP BY client;
-- email n'est pas dans GROUP BY ni dans une fonction d'agrégation

-- ✅ Correct
SELECT client, email, COUNT(*)
FROM commandes
GROUP BY client, email;

❌ Piège 2 : COUNT(*) vs COUNT(colonne)

-- COUNT(*) compte TOUTES les lignes (y compris celles avec NULL)
-- COUNT(email) compte uniquement les lignes où email n'est PAS NULL

SELECT 
    COUNT(*) AS total_lignes,
    COUNT(email) AS avec_email,
    COUNT(*) - COUNT(email) AS sans_email
FROM utilisateurs;

❌ Piège 3 : WHERE avec une fonction d'agrégation

-- ❌ ERREUR : les fonctions d'agrégation ne sont pas autorisées dans WHERE
SELECT client, SUM(montant)
FROM commandes
WHERE SUM(montant) > 1000
GROUP BY client;

-- ✅ Correct : utiliser HAVING
SELECT client, SUM(montant) AS total
FROM commandes
GROUP BY client
HAVING SUM(montant) > 1000;

❌ Piège 4 : GROUP BY avec JOIN multiplie les lignes

-- ❌ COUNT(*) est faussé si le JOIN crée des doublons
SELECT u.nom, COUNT(*) AS nb_commandes
FROM utilisateurs u
JOIN adresses a ON u.id = a.user_id    -- 2 adresses → double les lignes
JOIN commandes c ON u.id = c.user_id
GROUP BY u.id, u.nom;

-- ✅ Utiliser COUNT(DISTINCT)
SELECT u.nom, COUNT(DISTINCT c.id) AS nb_commandes
FROM utilisateurs u
JOIN adresses a ON u.id = a.user_id
JOIN commandes c ON u.id = c.user_id
GROUP BY u.id, u.nom;

❌ Piège 5 : AVG sur une colonne avec des NULL

-- Si la colonne "bonus" contient NULL pour 30% des employés,
-- AVG(bonus) calcule la moyenne sur 70% des employés seulement.
-- Pour inclure les NULL comme des 0 :
SELECT departement,
       AVG(COALESCE(bonus, 0)) AS bonus_moyen_reel
FROM employes
GROUP BY departement;
✅ Bonne pratique : GROUP BY sur l'id plutôt que sur le nom
Groupez toujours par la clé primaire (id) quand c'est possible. Deux clients pourraient avoir le même nom — grouper par nom les fusionnerait par erreur. PostgreSQL permet d'écrire GROUP BY id puis de mettre les autres colonnes dans le SELECT sans les lister dans le GROUP BY (car elles sont fonctionnellement dépendantes de l'id).
✅ Bonne pratique : WHERE avant HAVING pour la performance
Filtrez au maximum avec WHERE avant le regroupement. Un WHERE date > '2024-01-01' réduit les lignes à traiter par le GROUP BY, ce qui est plus rapide que de tout regrouper puis filtrer avec HAVING. En d'autres termes : WHERE travaille sur les données brutes (souvent indexées), HAVING travaille sur les données calculées.
✅ Bonne pratique : utiliser des index sur les colonnes de GROUP BY
Si vous faites souvent des GROUP BY client_id, assurez-vous que la colonne client_id est indexée. Le moteur SQL peut alors utiliser l'index pour regrouper les données plus efficacement, sans parcourir toute la table.

11. Questions fréquentes (FAQ)

Peut-on utiliser GROUP BY sans fonction d'agrégation ?
Techniquement oui, mais c'est équivalent à un SELECT DISTINCT. GROUP BY colonne sans agrégation retourne une ligne par valeur unique de la colonne. En pratique, préférez DISTINCT dans ce cas, qui exprime clairement l'intention.

-- Ces deux requêtes donnent le même résultat :
SELECT DISTINCT pays FROM clients;
SELECT pays FROM clients GROUP BY pays;
Quelle est la différence entre GROUP BY et PARTITION BY ?
GROUP BY réduit le nombre de lignes (une ligne par groupe). PARTITION BY s'utilise dans les fonctions de fenêtrage (OVER) et conserve toutes les lignes tout en calculant des agrégations par groupe. Exemple : afficher le salaire de chaque employé ET la moyenne du département sur la même ligne nécessite PARTITION BY, pas GROUP BY.

-- GROUP BY : une ligne par département
SELECT departement, AVG(salaire) FROM employes GROUP BY departement;

-- PARTITION BY : une ligne par employé avec la moyenne du département
SELECT nom, salaire, departement,
       AVG(salaire) OVER (PARTITION BY departement) AS moy_dept
FROM employes;
Comment paginer les résultats d'un GROUP BY ?
Utilisez LIMIT et OFFSET après le ORDER BY. Par exemple, pour la 2e page de 10 résultats :

SELECT client, SUM(montant) AS total
FROM commandes
GROUP BY client
ORDER BY total DESC
LIMIT 10 OFFSET 10;  -- page 2
Peut-on utiliser GROUP BY dans une sous-requête ?
Oui, c'est très courant. On peut utiliser une sous-requête agrégée dans le FROM (table dérivée) ou dans une CTE (WITH).

-- Top 3 clients par CA en utilisant une sous-requête
SELECT *
FROM (
    SELECT client, SUM(montant) AS total
    FROM commandes
    GROUP BY client
) AS ca_clients
WHERE total > 1000
ORDER BY total DESC
LIMIT 3;
GROUP BY préserve-t-il l'ordre des données ?
Non. GROUP BY ne garantit aucun ordre de tri. Pour trier les résultats, utilisez toujours ORDER BY explicitement. L'ordre apparent sans ORDER BY dépend du moteur et peut changer selon les versions ou les optimisations.
Comment gérer les valeurs NULL dans GROUP BY ?
Les valeurs NULL sont regroupées ensemble par GROUP BY : toutes les lignes où la colonne est NULL forment un seul groupe. Ce comportement est défini par le standard SQL. Si vous voulez remplacer NULL par une valeur par défaut, utilisez COALESCE dans le GROUP BY :

SELECT COALESCE(region, 'Non définie') AS region,
       COUNT(*) AS nb_clients
FROM clients
GROUP BY COALESCE(region, 'Non définie');

12. Quiz

Quelle est la différence entre WHERE et HAVING ?
WHERE filtre les lignes avant le GROUP BY. HAVING filtre les groupes après l'agrégation. On ne peut pas mettre de fonction d'agrégation dans WHERE.
Pourquoi cette requête est-elle fausse ? SELECT client, nom, COUNT(*) FROM commandes GROUP BY client;
La colonne nom n'est ni dans le GROUP BY ni dans une fonction d'agrégation. En PostgreSQL, c'est une erreur. Il faut ajouter nom au GROUP BY, ou l'enlever du SELECT, ou utiliser MAX(nom).
Quelle différence entre COUNT(*) et COUNT(email) ?
COUNT(*) compte toutes les lignes. COUNT(email) ne compte que les lignes où email n'est pas NULL.
Comment trouver les départements ayant plus de 10 employés avec un salaire moyen supérieur à 4000 ?
SELECT departement, COUNT(*) AS nb, AVG(salaire) AS moy
FROM employes
GROUP BY departement
HAVING COUNT(*) > 10 AND AVG(salaire) > 4000;
Peut-on mettre WHERE et HAVING dans la même requête ?
Oui, et c'est fréquent ! WHERE filtre d'abord les lignes, GROUP BY regroupe, puis HAVING filtre les groupes. Filtrer au maximum avec WHERE améliore les performances car le moteur traite moins de données lors de l'agrégation.
Quel est l'ordre d'exécution d'une requête SQL ?
FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT. C'est pour ça qu'on ne peut pas utiliser un alias SELECT dans WHERE (exécuté avant), mais qu'on peut dans ORDER BY (exécuté après).
GROUP BY avec un LEFT JOIN : comment éviter de fausser le COUNT ?
Utilisez COUNT(colonne_de_la_table_jointe) ou COUNT(DISTINCT id) au lieu de COUNT(*). Avec un LEFT JOIN, COUNT(*) comptera 1 même pour les lignes sans correspondance (la ligne existe avec NULL).
Quelle requête affiche les produits vendus par au moins 3 clients différents ?
SELECT produit, COUNT(DISTINCT client_id) AS nb_clients
FROM commandes
GROUP BY produit
HAVING COUNT(DISTINCT client_id) >= 3
ORDER BY nb_clients DESC;

COUNT(DISTINCT client_id) est essentiel ici : un même client peut avoir commandé le même produit plusieurs fois, et on ne veut compter chaque client qu'une seule fois par produit.

En résumé : GROUP BY et HAVING sont indispensables pour produire des statistiques et des rapports en SQL. Retenez les points clés : GROUP BY regroupe les lignes par valeur unique, les fonctions d'agrégation (COUNT, SUM, AVG, MIN, MAX) calculent une valeur par groupe, WHERE filtre avant l'agrégation (pour la performance), et HAVING filtre après. Maîtriser ces clauses vous permettra d'écrire des requêtes analytiques puissantes pour n'importe quelle base de données relationnelle. Pour aller plus loin, explorez les sous-requêtes et les fonctions de fenêtrage (OVER / PARTITION BY) qui complètent GROUP BY dans les analyses avancées.