aggregation

2025-09-01 07:55:42 +02:00
parent 6e54f5d61d
commit 12166825bb
5 changed files with 328 additions and 28 deletions

@@ -3,8 +3,8 @@
- [Fonctions d'agrégation](aggregation.md)
- [Jointure](Jointures.md)
- [Sous requêtes](sousrequete.md)
- [Arbres](Arbres.md)
- [Fonctions de fenêtrage](window.md)
- [Arbres](Arbres.md)
## Exercices
@@ -33,5 +33,3 @@ Arrondir les montant à deux chiffres après la virgule.
- 12b : Quels sont les articles qui apparaissent qu'une seule fois dans les lignes de ticket (Ananas séché 100g, ...)
- 13 : Lister les articles dont la famille est absente. (MENMA)
- 14 : Lister les tickets qui contiennent larticle le plus cher du catalogue.
- 20 : Quel adhérent est en double (même nom, prénom, date de naissance)

@@ -230,3 +230,16 @@ WHERE G.id <> D.id;
C'est une représentation purement théorique il n'y a pas de mot clé particulier, la différence est seulement que la même table apparaît plusieurs fois.
Ce type de requête est utilisées pour comparer des lignes entres elles ou détecter des doublons.
Quel adhérent est en double (même nom, prénom et date de naissance)
```sql
select a.nom, a.prenom
from adherent a
join adherent b on a.nom = b.nom
and a.prenom = b.prenom
and a.naissance = b.naissance
and a.id <> b.id
```
## Lateral

@@ -135,3 +135,270 @@ join article a on f.code = a.famille_code
join ligne l on a.code = l.article_code
where f.code = '03FRAISE'
group by f.code
Très bonne question 👍 ! Ici on veut, **pour chaque adhérent**, identifier **le ticket avec le montant total le plus élevé**.
On peut le résoudre **proprement avec les fonctions OLAP (fenêtrage)**.
---
## 🟢 Solution avec `ROW_NUMBER()`
```sql
SELECT adherent_id, id AS ticket_id, total
FROM (
SELECT
t.adherent_id,
t.id,
SUM(l.prix_unitaire * l.quantite) AS total,
ROW_NUMBER() OVER (
PARTITION BY t.adherent_id
ORDER BY SUM(l.prix_unitaire * l.quantite) DESC
) AS rn
FROM Ticket t
JOIN Ligne l ON l.ticket_id = t.id
GROUP BY t.adherent_id, t.id
) sub
WHERE rn = 1;
```
### ✅ Explications
1. On calcule le **total par ticket** avec `SUM(l.prix_unitaire * l.quantite)`.
2. Avec `ROW_NUMBER() OVER (PARTITION BY t.adherent_id ORDER BY total DESC)` :
* On numérote les tickets **par adhérent**.
* Le ticket avec le total le plus élevé prend `rn = 1`.
3. La requête finale ne garde que `rn = 1`, donc le **ticket max par adhérent**.
---
## 🟡 Variante avec `RANK()` (pour gérer les ex æquo)
Si un adhérent a **plusieurs tickets ex æquo** avec le même montant maximum, `ROW_NUMBER()` nen garde quun seul.
Si vous voulez les **tous**, utilisez `RANK()` :
```sql
SELECT adherent_id, id AS ticket_id, total
FROM (
SELECT
t.adherent_id,
t.id,
SUM(l.prix_unitaire * l.quantite) AS total,
RANK() OVER (
PARTITION BY t.adherent_id
ORDER BY SUM(l.prix_unitaire * l.quantite) DESC
) AS rk
FROM Ticket t
JOIN Ligne l ON l.ticket_id = t.id
GROUP BY t.adherent_id, t.id
) sub
WHERE rk = 1;
```
---
## 🟣 Variante simple avec sous-requêtes corrélées
Moins élégante, mais sans fonctions OLAP :
```sql
SELECT t.adherent_id, t.id AS ticket_id,
SUM(l.prix_unitaire * l.quantite) AS total
FROM Ticket t
JOIN Ligne l ON l.ticket_id = t.id
GROUP BY t.adherent_id, t.id
HAVING SUM(l.prix_unitaire * l.quantite) = (
SELECT MAX(total)
FROM (
SELECT SUM(l2.prix_unitaire * l2.quantite) AS total
FROM Ticket t2
JOIN Ligne l2 ON l2.ticket_id = t2.id
WHERE t2.adherent_id = t.adherent_id
GROUP BY t2.id
) s
);
```
---
👉 Préférez la version **fenêtrage (`ROW_NUMBER` ou `RANK`)**, bien plus claire et efficace.
Voulez-vous que je vous propose ensuite **la même logique mais pour les 3 tickets les plus chers par adhérent** (classement des top-N) ?
Très bonne question 👍 ! Ici on veut, **pour chaque adhérent**, identifier **le ticket avec le montant total le plus élevé**.
On peut le résoudre **proprement avec les fonctions OLAP (fenêtrage)**.
---
## 🟢 Solution avec `ROW_NUMBER()`
```sql
SELECT adherent_id, id AS ticket_id, total
FROM (
SELECT
t.adherent_id,
t.id,
SUM(l.prix_unitaire * l.quantite) AS total,
ROW_NUMBER() OVER (
PARTITION BY t.adherent_id
ORDER BY SUM(l.prix_unitaire * l.quantite) DESC
) AS rn
FROM Ticket t
JOIN Ligne l ON l.ticket_id = t.id
GROUP BY t.adherent_id, t.id
) sub
WHERE rn = 1;
```
### ✅ Explications
1. On calcule le **total par ticket** avec `SUM(l.prix_unitaire * l.quantite)`.
2. Avec `ROW_NUMBER() OVER (PARTITION BY t.adherent_id ORDER BY total DESC)` :
* On numérote les tickets **par adhérent**.
* Le ticket avec le total le plus élevé prend `rn = 1`.
3. La requête finale ne garde que `rn = 1`, donc le **ticket max par adhérent**.
---
## 🟡 Variante avec `RANK()` (pour gérer les ex æquo)
Si un adhérent a **plusieurs tickets ex æquo** avec le même montant maximum, `ROW_NUMBER()` nen garde quun seul.
Si vous voulez les **tous**, utilisez `RANK()` :
```sql
SELECT adherent_id, id AS ticket_id, total
FROM (
SELECT
t.adherent_id,
t.id,
SUM(l.prix_unitaire * l.quantite) AS total,
RANK() OVER (
PARTITION BY t.adherent_id
ORDER BY SUM(l.prix_unitaire * l.quantite) DESC
) AS rk
FROM Ticket t
JOIN Ligne l ON l.ticket_id = t.id
GROUP BY t.adherent_id, t.id
) sub
WHERE rk = 1;
```
---
## 🟣 Variante simple avec sous-requêtes corrélées
Moins élégante, mais sans fonctions OLAP :
```sql
SELECT t.adherent_id, t.id AS ticket_id,
SUM(l.prix_unitaire * l.quantite) AS total
FROM Ticket t
JOIN Ligne l ON l.ticket_id = t.id
GROUP BY t.adherent_id, t.id
HAVING SUM(l.prix_unitaire * l.quantite) = (
SELECT MAX(total)
FROM (
SELECT SUM(l2.prix_unitaire * l2.quantite) AS total
FROM Ticket t2
JOIN Ligne l2 ON l2.ticket_id = t2.id
WHERE t2.adherent_id = t.adherent_id
GROUP BY t2.id
) s
);
```
---
👉 Préférez la version **fenêtrage (`ROW_NUMBER` ou `RANK`)**, bien plus claire et efficace.
Voulez-vous que je vous propose ensuite **la même logique mais pour les 3 tickets les plus chers par adhérent** (classement des top-N) ?
Exactement !
Les **fonctions de fenêtrage** (aussi appelées **window functions**) sont parfaites pour ce genre de problème car elles permettent de classer ou de filtrer à lintérieur dun groupe sans avoir à écrire des sous-requêtes imbriquées lourdes.
---
## 🔹 Exemple : Le ticket le plus élevé par adhérent
Avec `ROW_NUMBER()` :
```sql
SELECT adherent_id, ticket_id, total
FROM (
SELECT
t.adherent_id,
t.id AS ticket_id,
SUM(l.prix_unitaire * l.quantite) AS total,
ROW_NUMBER() OVER (
PARTITION BY t.adherent_id
ORDER BY SUM(l.prix_unitaire * l.quantite) DESC
) AS rn
FROM Ticket t
JOIN Ligne l ON l.ticket_id = t.id
GROUP BY t.adherent_id, t.id
) sub
WHERE rn = 1;
```
* `PARTITION BY t.adherent_id` remet le compteur à zéro pour chaque adhérent.
* `ORDER BY total DESC` donne `rn = 1` au ticket le plus élevé.
* La clause externe `WHERE rn = 1` isole le meilleur ticket.
---
## 🔹 Variante : gérer les ex-æquo
Si deux tickets dun même adhérent ont exactement le même montant maximum, on peut utiliser `RANK()` (ou `DENSE_RANK()`) au lieu de `ROW_NUMBER()` :
```sql
SELECT adherent_id, ticket_id, total
FROM (
SELECT
t.adherent_id,
t.id AS ticket_id,
SUM(l.prix_unitaire * l.quantite) AS total,
RANK() OVER (
PARTITION BY t.adherent_id
ORDER BY SUM(l.prix_unitaire * l.quantite) DESC
) AS rk
FROM Ticket t
JOIN Ligne l ON l.ticket_id = t.id
GROUP BY t.adherent_id, t.id
) sub
WHERE rk = 1;
```
* `RANK()` donne le même rang aux ex-æquo (par exemple 1 et 1).
* `DENSE_RANK()` fait la même chose mais sans "sauter" de rang.
---
## 🔹 Variante bonus : les **3 meilleurs tickets** par adhérent
Très souvent demandé en pratique (Top-N) :
```sql
SELECT adherent_id, ticket_id, total
FROM (
SELECT
t.adherent_id,
t.id AS ticket_id,
SUM(l.prix_unitaire * l.quantite) AS total,
ROW_NUMBER() OVER (
PARTITION BY t.adherent_id
ORDER BY SUM(l.prix_unitaire * l.quantite) DESC
) AS rn
FROM Ticket t
JOIN Ligne l ON l.ticket_id = t.id
GROUP BY t.adherent_id, t.id
) sub
WHERE rn <= 3
ORDER BY adherent_id, total DESC;
```
---
👉 Voulez-vous que je prépare un **petit tableau récapitulatif** (avec exemples) qui compare `ROW_NUMBER()`, `RANK()` et `DENSE_RANK()` ? Cela permettrait de bien visualiser les différences pour les cas dex-æquo.

@@ -7,33 +7,22 @@
Compte le nombre d'enregistrements dans un ensemble de données.
```sql
select count(*) from table;
select count(*) from adherent;
```
On peut compter sur n'importe quelle colonne, dans ce cas, pour ne pas avoir à choisir on utilise le caractère joker *.
- 1 : Quel est le nombre total de ticket ? (101 616)
En ajoutant le mot clé distinct l'opération s'effectue sur les valeurs uniques d'une ou de plusieurs colonnes.
```sql
select count(*) as nb_ticket from ticket;
select count(distinct codepostal) from adherent;
```
- 2 : Quel est le nombre total d'adhérents ? (2439)
Il est également possible d'ajouter une condition WHERE à cette colonne uniquement. Cest souvent plus lisible et performant que de faire un CASE WHEN imbriqué.
```sql
select count(*) as nb_adherent from adherent;
```
- 3 : Quel est le nombre total d'articles ? (1099)
```sql
select count(*) as nb_articles from article;
```
- 4 : Quel est le nombre total de familles ? (138)
```sql
select count(*) as nb_familles from famille;
select count(distinct client) filter (where produit = 'pomme')
from vente;
```
### Somme
@@ -41,13 +30,14 @@ select count(*) as nb_familles from famille;
Calcule la somme des valeurs d'une colonne numérique.
```sql
SELECT SUM(colonne) FROM table;
SELECT SUM(vente) FROM ticket;
```
- 2 : Calculer le chiffre daffaires global. (1 914 792.40)
```sql
select round(sum(quantite * prix_unitaire), 2) as chiffre_affaire from ligne;
select sum(round(quantite * prix_unitaire, 2)) as chiffre_affaire from ligne;
```
@@ -56,7 +46,7 @@ select sum(round(quantite * prix_unitaire, 2)) as chiffre_affaire from ligne;
Calcule la moyenne des valeurs d'une colonne numérique.
```sql
SELECT AVG(salaire) FROM employes;
SELECT AVG(prix) FROM article;
```
### Maximum
@@ -64,7 +54,7 @@ SELECT AVG(salaire) FROM employes;
Retourne la valeur maximale dans une colonne.
```sql
SELECT MAX(age) FROM utilisateurs;
SELECT MAX(age) FROM adherent;
```
Une autre solution consiste à trier la colonne est de limiter le résultat à une seule ligne
@@ -81,7 +71,7 @@ order by prix desc limit 1;
Retourne la valeur minimale dans une colonne.
```sql
SELECT MIN(age) FROM utilisateurs;
SELECT MIN(age) FROM adherent;
```
Une autre solution consiste à trier la colonne est de limiter le résultat à une seule ligne
@@ -93,6 +83,19 @@ select article, prix from article
order by prix asc limit 1;
```
### Concaténation en chaine
`STRING_AGG` construit une chaine de caractères séparés par un délimiteur
```sql
select string_agg(distinct famille_code, ', ')
from article;
```
### Concaténation en tableau
`ARRAY_AGG` construit une tableau à partir des éléments. Très utilisé lors d'une sérialisation en JSON par exemple.
### Filtre après agrégation
`HAVING` est emblable à `WHERE`, mais utilisé pour filtrer les résultats des fonctions d'agrégation **après** un `GROUP BY`.
@@ -107,3 +110,5 @@ join ticket t on a.id = t.adherent_id
group by a.id
having count(t.id) = 72;
```
Avec postgreSQL et Microsoft SQL Server il n'est pas possible d'utiliser l'alias dans la condition having. La clause having intervient en amont du select dans le moteur d'exécution, l'alias n'est pas encore connu. Il faut réécrire la formule.

@@ -9,12 +9,25 @@ Elle est généralement entourée de parenthèses avec un nom d'alias et peut ap
- parfois dans HAVING
Il existe deux types de sous requêtes :
- La sous-requête corrélée qui dépend de la requête principale. Elle est donc réévaluée pour chaque ligne.
- La sous-requête non corréléé qui est indépendante et exécuté une seule fois.
- La sous-requête **corrélée** qui dépend de la requête principale. Elle est donc réévaluée pour chaque ligne.
- La sous-requête **non corréléé** qui est indépendante et exécuté une seule fois.
## Valeur
> Dans la clause FROM, une sous-requête est **toujours non corrélée**.
Parce quelle doit produire une table (un jeu de résultats) indépendante **avant** dêtre "reliée" au reste de la requête principale.
Une sous requête renvoie un ensemble de valeurs. Il est possible de renvoyer une seule valeur en limitant les résultats.
- Dans le SELECT, une sous-requête peut être corrélée (elle utilise des colonnes de la requête principale) ou non.
- Dans le WHERE, idem : elle peut être corrélée (EXISTS, IN dépendant de la ligne en cours) ou non (test fixe).
## Sous-requête scalaire
une sous-requête scalaire est une sous-requête qui retourne une seule valeur (un scalaire, cest-à-dire une seule ligne et une seule colonne).
Elle peut être utilisée partout où une valeur simple est attendue, par exemple :
- dans la liste de sélection (SELECT),
- dans une clause WHERE,
- dans une clause HAVING,
- parfois même dans la clause ORDER BY.
```sql
SELECT *
@@ -84,3 +97,7 @@ WHERE prix > ALL (
SELECT prix_promo FROM promotions
);
```
## Lateral
Une sous-requête LATERAL (ou sous-requête latérale) permet à une sous-requête dans la clause FROM daccéder aux colonnes déjà définies dans les relations qui la précèdent.