SQL Avancé : optimisation des bases de données
Jointures avancées, CTE récursives, fonctions de fenêtrage, index, EXPLAIN, isolation transactionnelle. Pour écrire des requêtes qui tiennent à l'échelle.
À la fin du cours, tu sais
- Maîtriser les jointures (LEFT, FULL, LATERAL) et les anti-jointures
- Utiliser CTE et CTE récursives pour des requêtes lisibles
- Exploiter les fonctions de fenêtrage (ROW_NUMBER, LAG, cumul)
- Lire un plan EXPLAIN ANALYZE pour diagnostiquer une requête lente
- Choisir le bon index (B-tree, partiel, composé, GIN)
- Comprendre les niveaux d'isolation et leurs anomalies
Prérequis
- Connaître les bases SQL (SELECT, INSERT, UPDATE, JOIN simple)
- Avoir accès à une base PostgreSQL ou MySQL pour tester
Chapitre 1
Jointures avancées
Tu connais INNER JOIN. On attaque les cas réels : données manquantes, anti-jointures, jointures latérales.
Anti-jointure : trouver ce qui manque
-- Clients sans aucune commande (anti-jointure)
SELECT c.id, c.nom
FROM clients c
LEFT JOIN commandes o ON o.client_id = c.id
WHERE o.id IS NULL;
-- Variante avec NOT EXISTS (souvent plus rapide)
SELECT c.id, c.nom
FROM clients c
WHERE NOT EXISTS (
SELECT 1 FROM commandes o WHERE o.client_id = c.id
);NOT IN avec NULL : piège classique
NOT IN (sous-requête) retourne 0 ligne dès qu'un NULL apparaît dans la sous-requête, à cause de la logique trivaluée SQL. Préfère NOT EXISTS, toujours.FULL OUTER JOIN
Garde toutes les lignes des deux côtés, NULL où il n'y a pas de correspondance. Utile pour réconcilier deux référentiels.
SELECT COALESCE(a.id, b.id) AS id, a.donnee_source, b.donnee_cible
FROM source a
FULL OUTER JOIN cible b ON a.id = b.id;LATERAL : sous-requête corrélée performante (Postgres)
-- Pour chaque client, les 3 dernières commandes
SELECT c.id, c.nom, d.id AS commande_id, d.total
FROM clients c
LEFT JOIN LATERAL (
SELECT id, total
FROM commandes
WHERE client_id = c.id
ORDER BY cree_le DESC
LIMIT 3
) d ON true;Chapitre 2
CTE : sous-requêtes lisibles et récursives
Les Common Table Expressions (CTE) clarifient les requêtes complexes et permettent la récursion sur des arborescences.
CTE simple
WITH top_clients AS (
SELECT client_id, SUM(total) AS ca
FROM commandes
WHERE cree_le >= '2026-01-01'
GROUP BY client_id
ORDER BY ca DESC
LIMIT 10
)
SELECT c.nom, t.ca
FROM top_clients t
JOIN clients c ON c.id = t.client_id;CTE inlinées par défaut depuis Postgres 12
WITH x AS MATERIALIZED (...).CTE récursive : parcourir une arborescence
-- Catégories en arborescence : parent_id pointe vers la catégorie parent
WITH RECURSIVE arbre AS (
-- Cas de base : les racines (sans parent)
SELECT id, parent_id, nom, 1 AS niveau
FROM categories
WHERE parent_id IS NULL
UNION ALL
-- Récursion : on prend les enfants des nœuds déjà trouvés
SELECT c.id, c.parent_id, c.nom, a.niveau + 1
FROM categories c
JOIN arbre a ON c.parent_id = a.id
)
SELECT * FROM arbre ORDER BY niveau, nom;Chapitre 3
Fonctions de fenêtrage
Calculer un classement, une moyenne glissante, un cumul, sans écraser les lignes. Pouvoir énorme, syntaxe à apprivoiser.
ROW_NUMBER, RANK, DENSE_RANK
-- Classement des commandes par client
SELECT
client_id,
id,
total,
ROW_NUMBER() OVER (PARTITION BY client_id ORDER BY total DESC) AS rang
FROM commandes;ROW_NUMBER: numéro séquentiel unique (1, 2, 3, 4...)RANK: ex-aequo ont le même rang, sauts ensuite (1, 2, 2, 4)DENSE_RANK: ex-aequo ont le même rang, sans sauts (1, 2, 2, 3)
LAG et LEAD : valeur précédente, suivante
-- Évolution du CA mois par mois
SELECT
mois,
ca,
LAG(ca) OVER (ORDER BY mois) AS ca_mois_precedent,
ca - LAG(ca) OVER (ORDER BY mois) AS variation
FROM ca_mensuel;Cumul et moyenne mobile
-- Cumul du CA depuis le début de l'année
SELECT
jour,
ca_jour,
SUM(ca_jour) OVER (ORDER BY jour) AS ca_cumule
FROM ca_journalier;
-- Moyenne glissante sur 7 jours
SELECT
jour,
ca_jour,
AVG(ca_jour) OVER (
ORDER BY jour
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS moyenne_7j
FROM ca_journalier;Chapitre 4
GROUP BY avancé
Aller au-delà du COUNT(*) : agrégats multidimensionnels et conditionnels.
GROUPING SETS, ROLLUP, CUBE
-- Sous-totaux par région ET par produit ET total général, en une passe
SELECT region, produit, SUM(ca) AS ca
FROM ventes
GROUP BY ROLLUP (region, produit);
-- Toutes les combinaisons (region, produit, region+produit, total)
SELECT region, produit, SUM(ca) AS ca
FROM ventes
GROUP BY CUBE (region, produit);Agrégats conditionnels avec FILTER (Postgres)
SELECT
region,
COUNT(*) FILTER (WHERE statut = 'PAYEE') AS payees,
COUNT(*) FILTER (WHERE statut = 'ANNULEE') AS annulees,
SUM(total) FILTER (WHERE statut = 'PAYEE') AS ca_paye
FROM commandes
GROUP BY region;FILTER plutôt que CASE
FILTER, on écrivait SUM(CASE WHEN ... THEN total END). C'est plus verbeux et moins lisible. FILTER est l'équivalent moderne et propre.Chapitre 5
Indexation : le levier numéro un
Un bon index transforme une requête de 30 secondes en 3 ms. Mais un mauvais index ralentit toutes tes écritures.
Les types d'index utiles
- B-tree : défaut, pour égalité et plages (
=,<,BETWEEN,ORDER BY) - Index composé : sur plusieurs colonnes, respecte l'ordre du WHERE
- Index partiel : ne couvre qu'une partie des lignes (ex :
WHERE actif = true) - GIN (Postgres) : pour
jsonb, tableaux, recherche plein-texte - BRIN (Postgres) : pour grosses tables triées (ex : logs par date)
Index composé : ordre des colonnes
-- Cas typique : on filtre par client et on trie par date
CREATE INDEX idx_cmd_client_date
ON commandes (client_id, cree_le DESC);
-- Cet index accélère :
SELECT * FROM commandes WHERE client_id = 42 ORDER BY cree_le DESC;
-- ET aussi : WHERE client_id = 42 (le préfixe gauche suffit)
-- Mais PAS : WHERE cree_le > '2026-01-01' tout seul
-- (la colonne client_id manque en préfixe)Index partiel
-- Si 99% des commandes sont 'PAYEE', un index global est inutile.
-- Mais on cherche souvent les 1% qui restent :
CREATE INDEX idx_cmd_pending
ON commandes (cree_le)
WHERE statut = 'EN_ATTENTE';
-- Index minuscule, ultra rapide sur les requêtes filtrées par statut.Coût des index
INSERT ou UPDATE de la colonne indexée. Sur une table qui écrit beaucoup, 10 index = 10× plus lent à l'écriture. Ne crée que les index dont tu as la preuve qu'ils servent (via EXPLAIN).Chapitre 6
EXPLAIN ANALYZE : lire un plan
Diagnostiquer une requête lente, c'est lire son plan d'exécution. Une compétence qui vaut de l'or.
EXPLAIN vs EXPLAIN ANALYZE
EXPLAIN requête: plan estimé, sans exécutionEXPLAIN ANALYZE requête: plan + exécution réelle + timingsEXPLAIN (ANALYZE, BUFFERS) requête: ajoute le détail des pages lues
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM commandes
WHERE client_id = 42 AND cree_le > NOW() - INTERVAL '30 days';Ce qu'il faut repérer
- Seq Scan sur grosse table : index manquant, presque toujours à corriger
- Index Scan : bon signe (le moteur utilise un index)
- Bitmap Heap Scan : convenable pour des filtres qui ramènent beaucoup de lignes
- Écart entre rows estimé et rows réel : statistiques à rafraîchir avec
ANALYZE table; - BUFFERS : shared read = lectures disque (lent).
shared hit= cache (rapide).
ANALYZE sur UPDATE/DELETE
EXPLAIN ANALYZE exécute vraiment la requête. Si c'est un UPDATE, ça modifie tes données. Encadre avec BEGIN; ... ROLLBACK; pour tester sans rien casser.Chapitre 7
Transactions, isolation, concurrence
Quand plusieurs sessions écrivent en même temps, des anomalies peuvent apparaître. Comprendre les niveaux d'isolation, c'est éviter les bugs invisibles.
ACID : les 4 garanties
- Atomicité : tout passe ou rien
- Cohérence : la base reste valide (contraintes respectées)
- Isolation : les transactions concurrentes ne se voient pas l'une l'autre
- Durabilité : un commit survit à un crash
Les niveaux d'isolation
READ COMMITTED(défaut Postgres) : tu lis toujours des données committéesREPEATABLE READ(défaut MySQL) : deux lectures successives renvoient le même résultat dans la transactionSERIALIZABLE: comme si les transactions s'exécutaient en série, sans concurrence
Verrouillage explicite : SELECT FOR UPDATE
BEGIN;
-- Verrouille la ligne jusqu'à COMMIT/ROLLBACK
SELECT solde FROM comptes WHERE id = 1 FOR UPDATE;
-- Calcul...
UPDATE comptes SET solde = solde - 100 WHERE id = 1;
COMMIT;Write skew : le piège du READ COMMITTED
SERIALIZABLE ou verrouillage explicite.🛠️ Exercice optionnel
Optimiser une requête lente
Tu travailles sur une base e-commerce : clients (500 k lignes), commandes (10 M lignes), lignes_commande (50 M lignes). Cette requête met 45 secondes en prod et ralentit toute l'app. À toi de la passer sous 2 secondes.
Ta mission
La requête fautive :
SELECT c.nom, COUNT(*) AS nb, SUM(lc.prix * lc.qte) AS ca
FROM clients c, commandes o, lignes_commande lc
WHERE c.id = o.client_id AND lc.commande_id = o.id
AND o.cree_le > '2026-01-01' AND c.pays = 'FR'
GROUP BY c.nom ORDER BY ca DESC LIMIT 20;
- Lance
EXPLAIN (ANALYZE, BUFFERS)pour identifier les Seq Scans. - Réécris la requête avec des
JOIN ... ONexplicites (plus lisible, même résultat). - Crée les index nécessaires pour transformer les Seq Scans en Index Scans.
- Lance à nouveau
EXPLAIN ANALYZEpour comparer le temps.
Tu bloques ? Des indices, à dévoiler quand tu en as besoin.
Indice 1
Indice masqué.
Indice 2
Indice masqué.
Indice 3
Indice masqué.
✅ QCM de fin de cours
Teste tes acquis
10 questions, plusieurs réponses parfois possibles. Coche tout ce qui te semble juste, puis valide pour voir ton score et les explications.
- 1
NOT IN (sous-requête)retourne souvent zéro ligne quand la sous-requête contient des NULL. Pourquoi ? - 2
ROW_NUMBER()etRANK()donnent-ils le même résultat sur des ex-aequo ? - 3
Un index sur
(a, b)accélère-t-il unWHERE b = ?seul ? - 4
Pourquoi un index partiel peut-il être plus utile qu'un index complet ?
- 5
EXPLAIN ANALYZEexécute vraiment la requête. Quel est le risque sur unUPDATE? - 6
Quel est le niveau d'isolation par défaut en PostgreSQL ?
- 7
Quel index est recommandé pour la recherche plein-texte sur un
tsvectoren Postgres ? - 8
SUM(montant) OVER (PARTITION BY client_id)renvoie combien de lignes ? - 9
Sur une grosse table triée naturellement (par exemple
logsinsérés en flux parcreated_at), quel type d'index Postgres est idéal ? - 10
FILTER (WHERE ...)remplace avantageusement quelle écriture ?
Tu peux laisser des questions sans réponse, elles compteront comme fausses.
Tu veux ce cours pour ton équipe ?
Je peux adapter et animer ce cours pour tes formateur·ices ou tes apprenant·es, en présentiel ou en distanciel. Parlons-en pendant l'audit gratuit.
Réserver un audit gratuit →