GevoxxInsights All articles
Software engineering · Databases

Accélérer PostgreSQL : index, EXPLAIN et pièges courants

La plupart des lenteurs SQL viennent de quelques causes récurrentes. Lire un plan d'exécution et poser les bons index change souvent tout.

PostgreSQL est une base de données remarquablement puissante et fiable, mais une requête mal pensée peut passer de quelques millisecondes à plusieurs secondes — et faire s'effondrer une application sous la charge. La bonne nouvelle : l'immense majorité des problèmes de performance se ramènent à une poignée de causes bien identifiées. Les connaître suffit à résoudre la plupart des ralentissements.

Règle d'or : mesurer avant d'optimiser

N'optimisez jamais à l'aveugle. La commande EXPLAIN ANALYZE exécute réellement la requête et montre le plan d'exécution choisi par le planificateur, avec les temps réels :

EXPLAIN ANALYZE
SELECT * FROM invoices WHERE client_id = 42;

Le mot à repérer est Seq Scan (parcours séquentiel) sur une grande table : PostgreSQL lit toutes les lignes une par une pour en garder quelques-unes. Sur une petite table c'est normal et optimal ; sur une table volumineuse et filtrée, c'est le signe qu'il manque un index. À l'inverse, un Index Scan indique un accès ciblé.

Les index, avec discernement

Un index B-tree sur la colonne filtrée transforme le parcours complet en accès quasi direct :

CREATE INDEX idx_invoices_client ON invoices (client_id);
Un index accélère les lectures mais ralentit les écritures (il faut le maintenir) et occupe de l'espace : on indexe ce qui est réellement filtré ou trié, pas tout « au cas où ».

Deux techniques avancées très rentables : les index composites (sur plusieurs colonnes, dans le bon ordre) pour les requêtes multi-critères, et les index couvrants (clause INCLUDE) qui permettent de répondre sans même lire la table (« Index Only Scan »).

Le piège N+1 : l'ennemi côté application

L'erreur la plus fréquente ne vient pas de SQL mais de l'ORM : le problème N+1. Une requête récupère une liste de 100 factures, puis le code déclenche une requête par facture pour charger son client. Résultat : 101 allers-retours là où un seul suffirait. La solution : charger les données liées en une seule requête (jointure, ou chargement groupé — « eager/batch loading » selon l'ORM). C'est souvent le gain de performance le plus spectaculaire d'une application.

Autres réflexes qui paient

  • Évitez SELECT * : ne récupérez que les colonnes nécessaires (moins d'I/O, index couvrants possibles).
  • Méfiez-vous des fonctions sur une colonne indue dans le WHERE (ex. WHERE lower(email) = ...) : elles annulent l'index, sauf à créer un index d'expression.
  • Surveillez les statistiques du planificateur et lancez ANALYZE après de gros changements de données ; laissez l'autovacuum faire son travail.
  • Activez l'extension pg_stat_statements pour repérer les requêtes les plus coûteuses en production.
  • Pensé à la pagination par curseur (keyset) plutôt que OFFSET sur de grandes tables.

En résumé

Optimiser une base n'est pas de la magie : c'est un cycle discipline — mesurer (EXPLAIN), comprendre le plan, corriger la cause (index, requête, N+1), puis remesurer. On corrige la cause racine, on n'empile pas les rustines.

Sources et références