GRIMOIRE DU MOUSSE

LE GRIMOIRE
COMPLET

Toute la lecture SQL, de SELECT aux fonctions fenêtre. Chaque leçon a sa requête et son résultat ; les ateliers en enchaînent plusieurs. Lis, copie, va la casser dans le bac à sable.

33 LEÇONS

01

Lire des colonnes

SELECT / FROM

SELECT choisit les colonnes, FROM la table. C’est le squelette de toute requête de lecture.

SELECT name, tonnage
FROM vessels;

SELECT * renvoie toutes les colonnes — pratique pour explorer, à éviter en production.

Résultat
nametonnage
Bonne Étoile120
Vent Fou95
Ombre Grise140
Aigle des Mers110
Fille du Nord160
S'entraîner →
02

Renommer une colonne

AS

AS donne un nom d’affichage à une colonne ou à un calcul. Le résultat est plus lisible et les alias servent de raccourci dans le reste de la requête.

SELECT name AS navire, tonnage AS poids
FROM vessels;

AS est optionnel : `tonnage poids` marche aussi. Les guillemets doubles gardent espaces et majuscules : AS "Poids en tonnes".

Résultat
navirepoids
Bonne Étoile120
Vent Fou95
Ombre Grise140
Aigle des Mers110
Fille du Nord160
S'entraîner →
03

Valeurs uniques

DISTINCT

DISTINCT retire les doublons du résultat. Utile pour répondre à « quelles valeurs existent ? ».

SELECT DISTINCT category
FROM goods;

DISTINCT porte sur toutes les colonnes listées : `DISTINCT category, name` garde les paires uniques, pas les catégories uniques.

Résultat
category
arme
boisson
denrée
matériel
outil
S'entraîner →
04

Filtrer les lignes

WHERE

WHERE ne garde que les lignes qui remplissent la condition. Opérateurs : = <> < > <= >=.

SELECT name, tonnage
FROM vessels
WHERE tonnage > 100;

Le texte se compare entre apostrophes : WHERE status = 'livré'. `<>` (ou `!=`) veut dire « différent de ».

Résultat
nametonnage
Bonne Étoile120
Ombre Grise140
Aigle des Mers110
Fille du Nord160
S'entraîner →
05

Combiner des conditions

AND / OR / NOT

AND exige les deux conditions, OR au moins une, NOT inverse. On en enchaîne autant qu’on veut.

SELECT name, tonnage, port_id
FROM vessels
WHERE tonnage >= 100 AND port_id = 2;

AND est évalué avant OR : mets des parenthèses au moindre doute — WHERE a AND (b OR c).

Résultat
nametonnageport_id
Ombre Grise1402
Fille du Nord1602
S'entraîner →
06

Plages et listes

BETWEEN / IN

BETWEEN teste un intervalle, IN une liste de valeurs. Plus court et plus clair qu’une chaîne de OR.

SELECT name, tonnage
FROM vessels
WHERE tonnage BETWEEN 90 AND 120;

BETWEEN inclut les deux bornes. `port_id IN (1, 2)` = port_id = 1 OR port_id = 2 ; `NOT IN (…)` pour exclure.

Résultat
nametonnage
Bonne Étoile120
Vent Fou95
Aigle des Mers110
Longue Vue100
S'entraîner →
07

Chercher par motif

LIKE / ILIKE

LIKE compare du texte à un motif : `%` remplace n’importe quelle suite de caractères, `_` un seul.

SELECT name
FROM ports
WHERE name LIKE 'C%';

ILIKE ignore la casse (propre à PostgreSQL). Pour un `%` littéral, il faut l’échapper.

Résultat
name
Cap-Gris
S'entraîner →
08

Gérer l’absence de valeur

NULL

NULL veut dire « inconnu ». Il ne vaut rien, pas même un autre NULL, d’où un test dédié.

SELECT name
FROM captains
WHERE home_port_id IS NULL;

Toujours IS NULL / IS NOT NULL — jamais `= NULL`, qui ne renvoie jamais vrai.

Résultat
name
Aro Kesh
S'entraîner →
09

Remplacer les NULL

COALESCE

COALESCE renvoie le premier de ses arguments qui n’est pas NULL — parfait pour afficher une valeur de repli.

SELECT name, COALESCE(home_port_id, 0) AS port_ou_zero
FROM captains;

`NULLIF(a, b)` fait l’inverse : il renvoie NULL quand a = b.

Résultat
nameport_ou_zero
Crow1
Lina Sorne1
Milo Vantar3
Talia Rœ4
Nils Harg5
S'entraîner →
10

Colonnes conditionnelles

CASE

CASE choisit une valeur selon des conditions, comme un if/else. Il se termine par END.

SELECT name,
       CASE WHEN tonnage >= 120 THEN 'lourd'
            WHEN tonnage >= 90  THEN 'moyen'
            ELSE 'léger' END AS gabarit
FROM vessels;

CASE s’utilise partout : SELECT, WHERE, ORDER BY, et dans un agrégat — sum(CASE WHEN … THEN 1 ELSE 0 END).

Résultat
namegabarit
Bonne Étoilelourd
Vent Foumoyen
Ombre Griselourd
Aigle des Mersmoyen
Fille du Nordlourd
S'entraîner →
11

Calculer dans le SELECT

Expressions

Une colonne peut être un calcul, pas seulement un champ brut. Les opérateurs : + - * / %.

SELECT name, unit_price,
       round(unit_price * 1.2, 2) AS prix_ttc
FROM goods;

Concaténer du texte : `name || ' — ' || category`. `round(x, n)` arrondit à n décimales.

Résultat
nameunit_priceprix_ttc
Rhum ambré8.5010.20
Toile de voile15.0018.00
Corde chanvre3.253.90
Épices22.4026.88
Sucre roux6.107.32
S'entraîner →
12

Manipuler le texte

Fonctions

PostgreSQL fournit des dizaines de fonctions : sur le texte, upper/lower changent la casse, length compte les caractères.

SELECT upper(name) AS majuscule, length(name) AS taille
FROM regions;

Autres utiles : trim, substr(x, 1, 3), replace(x, 'a', 'b'), left(x, 4), position('-' in x).

Résultat
majusculetaille
CÔTE DE FER11
ARCHIPEL SUD12
MER BLANCHE11
S'entraîner →
13

Trier le résultat

ORDER BY

ORDER BY range les lignes : ASC (croissant) par défaut, DESC pour l’inverse.

SELECT name, tonnage
FROM vessels
ORDER BY tonnage DESC;

Tri à plusieurs niveaux : `ORDER BY category, unit_price DESC`. `NULLS LAST` renvoie les vides en fin de liste.

Résultat
nametonnage
Fille du Nord160
Ombre Grise140
Bonne Étoile120
Aigle des Mers110
Longue Vue100
S'entraîner →
14

Limiter et paginer

LIMIT / OFFSET

LIMIT plafonne le nombre de lignes renvoyées. OFFSET en saute au début — les deux servent à paginer.

SELECT name, tonnage
FROM vessels
ORDER BY tonnage DESC
LIMIT 3;

`LIMIT 10 OFFSET 20` = la 3ᵉ page de 10. Sans ORDER BY, « les 3 premières » n’a aucun sens : l’ordre n’est pas garanti.

Résultat
nametonnage
Fille du Nord160
Ombre Grise140
Bonne Étoile120
S'entraîner →
15

Compter, additionner, moyenner

Agrégats

Les fonctions d’agrégat réduisent plusieurs lignes à une seule valeur : count, sum, avg, min, max.

SELECT count(*) AS navires,
       round(avg(tonnage), 1) AS tonnage_moyen,
       max(tonnage) AS plus_gros
FROM vessels;

count(*) compte les lignes ; count(colonne) ignore les NULL de cette colonne.

Résultat
navirestonnage_moyenplus_gros
8108.8160
S'entraîner →
16

Un résultat par groupe

GROUP BY

GROUP BY réunit les lignes qui partagent une valeur, puis les agrégats calculent une valeur par groupe.

SELECT status, count(*) AS total
FROM shipments
GROUP BY status;

Règle d’or : toute colonne du SELECT qui n’est pas dans un agrégat doit figurer dans le GROUP BY.

Résultat
statustotal
en mer4
livré7
perdu1
S'entraîner →
17

Filtrer après regroupement

HAVING

HAVING est un WHERE qui s’applique aux groupes, une fois les agrégats calculés.

SELECT port_id, count(*) AS navires
FROM vessels
GROUP BY port_id
HAVING count(*) > 1;

WHERE filtre les lignes AVANT le GROUP BY, HAVING filtre les groupes APRÈS. Les deux peuvent coexister.

Résultat
port_idnavires
12
22
S'entraîner →
18

Croiser deux tables

JOIN

JOIN relie deux tables sur une colonne commune, ici l’id du port rangé dans vessels.port_id.

SELECT v.name AS navire, p.name AS port
FROM vessels v
JOIN ports p ON p.id = v.port_id;

JOIN seul = INNER JOIN : seules les lignes qui ont une correspondance des deux côtés ressortent.

Résultat
navireport
Bonne ÉtoilePort-Brume
Vent FouAnse-du-Roi
Ombre GriseHavre-Noir
Aigle des MersCap-Gris
Fille du NordHavre-Noir
S'entraîner →
19

Garder les lignes sans correspondance

LEFT / RIGHT / FULL JOIN

LEFT JOIN garde toutes les lignes de la table de gauche ; les colonnes de droite valent NULL quand rien ne correspond.

SELECT v.name, c.name AS capitaine
FROM vessels v
LEFT JOIN captains c ON c.id = v.captain_id;

RIGHT JOIN fait l’inverse, FULL JOIN garde les deux côtés. `WHERE c.id IS NULL` isole les lignes sans match.

Résultat
namecapitaine
Bonne ÉtoileCrow
Vent FouMilo Vantar
Ombre GriseNULL
Aigle des MersNils Harg
Fille du NordBeno Trask
S'entraîner →
20

Enchaîner les jointures

JOIN (chaîné)

On enchaîne autant de JOIN que nécessaire pour remonter une chaîne de tables : navire → capitaine → port → région.

SELECT v.name AS navire, c.name AS capitaine, r.name AS region
FROM vessels v
JOIN captains c ON c.id = v.captain_id
JOIN ports p ON p.id = c.home_port_id
JOIN regions r ON r.id = p.region_id;

Chaque JOIN a sa condition ON. L’ordre des JOIN ne change pas le résultat d’un INNER JOIN, seulement la lisibilité.

Résultat
navirecapitaineregion
Bonne ÉtoileCrowCôte de Fer
Vent FouMilo VantarArchipel Sud
Aigle des MersNils HargMer Blanche
Fille du NordBeno TraskCôte de Fer
Sœur RougeLina SorneCôte de Fer
S'entraîner →
21

Joindre une table à elle-même

Self-join

Quand une ligne pointe deux fois vers la même table — ici un départ et une arrivée dans ports — on joint ports deux fois, sous deux alias.

SELECT cr.id,
       dep.name AS depart,
       arr.name AS arrivee
FROM crossings cr
JOIN ports dep ON dep.id = cr.from_port_id
JOIN ports arr ON arr.id = cr.to_port_id;

Sans les alias dep / arr, PostgreSQL ne saurait pas de quelle copie de ports on parle.

Résultat
iddepartarrivee
1Port-BrumeAnse-du-Roi
2Anse-du-RoiRécif-Clair
3Cap-GrisPort-Brume
4Récif-ClairBaie-Lune
5Port-BrumeHavre-Noir
S'entraîner →
22

Empiler deux résultats

UNION

UNION met bout à bout les lignes de deux SELECT qui ont les mêmes colonnes.

SELECT name FROM ports
UNION
SELECT name FROM regions
ORDER BY name;

UNION dédoublonne ; `UNION ALL` garde tout (et va plus vite). Voir aussi INTERSECT (commun) et EXCEPT (différence).

Résultat
name
Anse-du-Roi
Archipel Sud
Baie-Lune
Cap-Gris
Côte de Fer
S'entraîner →
23

Une requête dans le WHERE

Sous-requête

Une sous-requête entre parenthèses est calculée d’abord ; sa valeur sert ensuite de condition.

SELECT name, tonnage
FROM vessels
WHERE tonnage > (SELECT avg(tonnage) FROM vessels);

Si la sous-requête peut renvoyer plusieurs lignes, utilise `IN (SELECT …)` au lieu de `= (SELECT …)`.

Résultat
nametonnage
Bonne Étoile120
Ombre Grise140
Aigle des Mers110
Fille du Nord160
S'entraîner →
24

Sous-requête corrélée

EXISTS

EXISTS répond « y a-t-il au moins une ligne ? ». Corrélée : la sous-requête cite la ligne courante (p.id).

SELECT p.name
FROM ports p
WHERE EXISTS (
SELECT   1 FROM vessels v WHERE v.port_id = p.id
);

EXISTS s’arrête au premier match, donc il est souvent rapide. `NOT EXISTS` pour « aucune ligne ».

Résultat
name
Anse-du-Roi
Cap-Gris
Récif-Clair
Baie-Lune
Havre-Noir
S'entraîner →
25

Une sous-requête comme table

Table dérivée

Dans le FROM, une sous-requête se comporte comme une table temporaire. Elle doit avoir un alias.

SELECT region_id, count(*) AS nb_ports_anciens
FROM (
SELECT   id, region_id FROM ports WHERE founded_year < 1750
) AS anciens
GROUP BY region_id;

Souvent remplaçable par une CTE `WITH`, qui se lit de haut en bas plutôt que de l’intérieur vers l’extérieur.

Résultat
region_idnb_ports_anciens
12
22
31
S'entraîner →
26

Nommer une étape avec WITH

WITH (CTE)

Une CTE (Common Table Expression) est un résultat intermédiaire nommé, défini en tête de requête et réutilisé ensuite.

WITH livraisons AS (
SELECT   vessel_id, count(*) AS n
FROM   shipments
WHERE   status = 'livré'
GROUP BY   vessel_id
)
SELECT v.name, livraisons.n
FROM livraisons
JOIN vessels v ON v.id = livraisons.vessel_id
ORDER BY livraisons.n DESC, v.name;

On chaîne plusieurs CTE séparées par des virgules. `WITH RECURSIVE` permet même de parcourir une hiérarchie.

Résultat
namen
Bonne Étoile2
Fille du Nord1
Longue Vue1
Petit Vif1
Sœur Rouge1
S'entraîner →
27

Fonctions fenêtre

OVER / PARTITION BY

Une fonction fenêtre calcule sur un groupe de lignes MAIS garde chaque ligne — la valeur s’affiche à côté.

SELECT name, port_id, tonnage,
       round(avg(tonnage) OVER (PARTITION BY port_id), 1) AS moy_du_port
FROM vessels;

PARTITION BY découpe la fenêtre en groupes ; sans lui, la fenêtre couvre toute la table.

Résultat
nameport_idtonnagemoy_du_port
Sœur Rouge185102.5
Bonne Étoile1120102.5
Fille du Nord2160150.0
Ombre Grise2140150.0
Vent Fou39595.0
S'entraîner →
28

Classer avec des fenêtres

ROW_NUMBER / RANK

row_number, rank et dense_rank numérotent les lignes selon un ORDER BY à l’intérieur de la fenêtre.

SELECT name, tonnage,
       rank() OVER (ORDER BY tonnage DESC) AS rang
FROM vessels;

row_number() : numéro unique. rank() : laisse un trou après les ex æquo. dense_rank() : sans trou.

Résultat
nametonnagerang
Fille du Nord1601
Ombre Grise1402
Bonne Étoile1203
Aigle des Mers1104
Longue Vue1005
S'entraîner →
29

Travailler avec les dates

Dates

Les dates se comparent avec < > =, et se décomposent avec extract(). CURRENT_DATE donne le jour même.

SELECT id, shipped_on,
       extract(month FROM shipped_on) AS mois
FROM shipments
WHERE shipped_on >= DATE '2024-05-01'
ORDER BY shipped_on;

Soustraire deux dates donne un nombre de jours : `arrived_on - departed_on`. `age(a, b)` donne un intervalle lisible.

Résultat
idshipped_onmois
92024-05-055
22024-05-185
32024-05-205
52024-06-016
62024-06-156
S'entraîner →
30

Atelier : les jointures

Atelier

Quatre requêtes pour ancrer les JOIN. Lis la solution, puis refais-la de mémoire dans le bac à sable.

SELECT v.name AS navire, p.name AS port
FROM vessels v
JOIN ports p ON p.id = v.port_id
ORDER BY v.name;

Modifie une seule chose à la fois — une table, une condition ON, un type de jointure — pour voir ce que chacune change.

Résultat
navireport
Aigle des MersCap-Gris
Bonne ÉtoilePort-Brume
Fille du NordHavre-Noir
Longue VueBaie-Lune
Ombre GriseHavre-Noir
S'entraîner →
À toi — 4 requêtes
01

Navire → capitaine

Le nom de chaque navire avec celui de son capitaine (uniquement ceux qui en ont un).

Voir la solution
SELECT v.name AS navire, c.name AS capitaine
FROM vessels v
JOIN captains c ON c.id = v.captain_id
ORDER BY v.name;
navirecapitaine
Aigle des MersNils Harg
Bonne ÉtoileCrow
Fille du NordBeno Trask
Longue VueYara Bel
Petit VifTalia Rœ
02

Navires sans capitaine

Les navires qui ne sont commandés par personne.

Voir la solution
SELECT v.name
FROM vessels v
LEFT JOIN captains c ON c.id = v.captain_id
WHERE c.id IS NULL;
name
Ombre Grise
03

Expédition → région de destination

Chaque expédition avec la région de son port de destination.

Voir la solution
SELECT s.id, r.name AS region
FROM shipments s
JOIN ports p ON p.id = s.port_id
JOIN regions r ON r.id = p.region_id
ORDER BY s.id;
idregion
1Archipel Sud
2Archipel Sud
3Côte de Fer
4Mer Blanche
5Côte de Fer
04

Ports sans navire

Les ports qui ne servent de port d’attache à aucun navire.

Voir la solution
SELECT p.name
FROM ports p
LEFT JOIN vessels v ON v.port_id = p.id
WHERE v.id IS NULL;
name
31

Atelier : agréger et regrouper

Atelier

COUNT, SUM, AVG, GROUP BY, HAVING réunis sur un même jeu de données. Cinq gestes, une base.

SELECT category, count(*) AS lignes, round(avg(unit_price), 2) AS prix_moyen
FROM goods
GROUP BY category
ORDER BY category;

Un agrégat sans GROUP BY renvoie une ligne pour toute la table ; avec, une ligne par groupe.

Résultat
categorylignesprix_moyen
arme130.00
boisson18.50
denrée310.17
matériel39.35
outil228.88
S'entraîner →
À toi — 4 requêtes
01

Navires par port

Combien de navires sont amarrés dans chaque port ?

Voir la solution
SELECT port_id, count(*) AS navires
FROM vessels
GROUP BY port_id
ORDER BY port_id;
port_idnavires
12
22
31
41
51
02

Valeur transportée par catégorie

Somme de quantité × prix unitaire, par catégorie de marchandise.

Voir la solution
SELECT g.category, sum(si.quantity * g.unit_price) AS valeur
FROM shipment_items si
JOIN goods g ON g.id = si.goods_id
GROUP BY g.category
ORDER BY valeur DESC;
categoryvaleur
denrée3491.80
matériel2878.50
arme1500.00
outil1498.50
boisson1147.50
03

Ports à plusieurs expéditions

Les ports qui ont reçu strictement plus d’une expédition.

Voir la solution
SELECT port_id, count(*) AS n
FROM shipments
GROUP BY port_id
HAVING count(*) > 1
ORDER BY n DESC, port_id;
port_idn
33
12
42
52
62
04

Réputation moyenne par port d’attache

La réputation moyenne des capitaines, regroupée par port d’attache.

Voir la solution
SELECT home_port_id, round(avg(reputation), 1) AS rep_moyenne
FROM captains
WHERE home_port_id IS NOT NULL
GROUP BY home_port_id
ORDER BY home_port_id;
home_port_idrep_moyenne
163.5
290.0
381.0
440.0
563.0
32

Atelier : sous-requêtes & CTE

Atelier

Décomposer un problème : une requête calcule une valeur ou un ensemble, une seconde s’appuie dessus.

SELECT name, tonnage
FROM vessels
WHERE tonnage > (SELECT avg(tonnage) FROM vessels)
ORDER BY tonnage DESC;

Dès qu’une sous-requête devient dure à lire, sors-la dans une CTE `WITH` en tête : même résultat, plus clair.

Résultat
nametonnage
Fille du Nord160
Ombre Grise140
Bonne Étoile120
Aigle des Mers110
S'entraîner →
À toi — 4 requêtes
01

Au-dessus du prix moyen

Les marchandises plus chères que le prix unitaire moyen.

Voir la solution
SELECT name, unit_price
FROM goods
WHERE unit_price > (SELECT avg(unit_price) FROM goods)
ORDER BY unit_price DESC;
nameunit_price
Cartes marines45.00
Poudre noire30.00
Épices22.40
02

Ports avec au moins un navire (EXISTS)

Les ports qui abritent au moins un navire, testé avec EXISTS.

Voir la solution
SELECT p.name
FROM ports p
WHERE EXISTS (SELECT 1 FROM vessels v WHERE v.port_id = p.id)
ORDER BY p.name;
name
Anse-du-Roi
Baie-Lune
Cap-Gris
Havre-Noir
Port-Brume
03

Nombre d’expéditions par navire (CTE)

Le nombre d’expéditions de chaque navire, calculé dans une CTE.

Voir la solution
WITH par_navire AS (
SELECT   vessel_id, count(*) AS n
FROM   shipments
GROUP BY   vessel_id
)
SELECT v.name, par_navire.n
FROM par_navire
JOIN vessels v ON v.id = par_navire.vessel_id
ORDER BY par_navire.n DESC, v.name;
namen
Bonne Étoile3
Aigle des Mers2
Fille du Nord2
Vent Fou2
Longue Vue1
04

Capitaines sans navire

Les capitaines qui ne commandent aucun navire.

Voir la solution
SELECT c.name
FROM captains c
WHERE c.id NOT IN (SELECT captain_id FROM vessels WHERE captain_id IS NOT NULL)
ORDER BY c.name;
name
Aro Kesh
33

Au-delà de la lecture

Écriture

SQL sert aussi à écrire et à définir des tables. Le bac à sable est en lecture seule, mais ces commandes font partie du langage.

-- Modifier les données
INSERT INTO goods (id, name, category, unit_price) VALUES (11, 'Thé', 'boisson', 5.0);
UPDATE goods SET unit_price = 6.0 WHERE id = 11;
DELETE FROM goods WHERE id = 11;

Structure : CREATE TABLE, ALTER TABLE, DROP TABLE. Transactions : BEGIN … COMMIT / ROLLBACK. Ici, on ne pratique que SELECT.

Résultat

Pas de résultat — le bac à sable est en lecture seule.

S'entraîner →