Comprendre pourquoi une requête SQL est lente commence toujours par la même commande : EXPLAIN. Cet outil intégré à la quasi-totalité des SGBD modernes vous révèle exactement comment le moteur de base de données prévoit d'exécuter votre requête, avant même qu'elle ne tourne. En entretien technique comme en production, savoir lire un plan d'exécution est une compétence qui sépare les développeurs data ordinaires des profils vraiment solides.
Dans cet article, vous apprendrez à utiliser EXPLAIN (et sa variante EXPLAIN ANALYZE) pour identifier les goulots d'étranglement, repérer les scans de table inutiles, et comprendre pourquoi l'optimiseur choisit — ou évite — un index. Des exemples concrets en PostgreSQL et MySQL illustrent chaque concept.
📌 Ce qu'il faut retenir
EXPLAINaffiche le plan d'exécution estimé sans exécuter la requêteEXPLAIN ANALYZEexécute réellement la requête et compare estimations vs réalité- Un Sequential Scan sur une grande table est souvent le signe d'un index manquant
- Lire le plan de bas en haut : le nœud le plus indenté s'exécute en premier
Ce que fait réellement EXPLAIN
Quand vous préfixez une requête avec EXPLAIN, le moteur SQL ne l'exécute pas. Il demande à l'optimiseur de produire le plan d'exécution qu'il utiliserait, puis l'affiche sous forme d'arbre d'opérations. Chaque nœud correspond à une étape : lecture d'une table, application d'un filtre, fusion de deux ensembles via une jointure, etc.
L'optimiseur s'appuie sur des statistiques internes (distribution des valeurs, cardinalité des tables, présence d'index) pour choisir le plan le moins coûteux. Ce coût est une estimation abstraite, pas des millisecondes — mais il vous indique quelles opérations sont jugées "chères" par le moteur.
En PostgreSQL, la syntaxe de base est simple :
EXPLAIN SELECT * FROM commandes WHERE client_id = 42;
La sortie ressemble à ceci :
Seq Scan on commandes (cost=0.00..4523.00 rows=18 width=96)
Filter: (client_id = 42)
Ici, le moteur prévoit un Sequential Scan (lecture ligne par ligne) de toute la table. Si commandes contient des millions de lignes, c'est un problème évident.
EXPLAIN ANALYZE : la version avec données réelles
EXPLAIN ANALYZE va plus loin : il exécute la requête et ajoute les temps réels à côté des estimations. C'est indispensable pour détecter les cas où l'optimiseur sous-estime ou surévalue la cardinalité d'un résultat.
EXPLAIN ANALYZE SELECT * FROM commandes WHERE client_id = 42;
Sortie typique :
Seq Scan on commandes (cost=0.00..4523.00 rows=18 width=96)
(actual time=0.043..312.8 rows=2341 loops=1)
Filter: (client_id = 42)
Rows Removed by Filter: 198432
Planning Time: 0.12 ms
Execution Time: 315.3 ms
Deux signaux d'alarme apparaissent ici. D'abord, l'optimiseur estimait 18 lignes retournées, mais 2 341 ont été trouvées : les statistiques sont obsolètes. Ensuite, 198 432 lignes ont été filtrées après lecture — autant de travail inutile qui justifie un index sur client_id.
⚠️ Attention
Sur une table de production, EXPLAIN ANALYZE exécute réellement la requête. Évitez de l'utiliser sur des requêtes DELETE ou UPDATE sans les entourer d'une transaction que vous annulez avec ROLLBACK.
Lire un plan d'exécution : les nœuds essentiels
Un plan d'exécution est un arbre. La règle de lecture est constante : commencez par le nœud le plus indenté (le plus profond), car c'est lui qui s'exécute en premier. Les résultats remontent ensuite vers le nœud parent.
Voici les nœuds que vous rencontrerez le plus souvent :
| Nœud | Description | Signal |
|---|---|---|
| Seq Scan | Lecture séquentielle de toute la table | ⚠️ Coûteux sur grandes tables sans index |
| Index Scan | Utilisation d'un index B-tree pour localiser les lignes | ✅ Efficace pour sélectivité élevée |
| Index Only Scan | Réponse entière fournie par l'index (covering index) | ✅ Optimal, évite l'accès à la table |
| Bitmap Heap Scan | Combine plusieurs index pour filtrer | 🔄 Bon compromis sur cardinalité moyenne |
| Hash Join | Jointure via table de hachage en mémoire | ✅ Efficace sur grandes tables sans index de jointure |
| Nested Loop | Boucle imbriquée pour chaque ligne du jeu externe | ⚠️ Mauvais si le jeu interne est grand |
| Merge Join | Fusion de 2 ensembles triés | ✅ Très efficace si les données sont déjà triées |
| Sort | Tri explicite (ORDER BY, DISTINCT, Merge Join) | ⚠️ Coûteux si trop de lignes à trier |
