Choisir le bon index PostgreSQL

PostgreSQL fournit six méthodes d'accès aux index, et CREATE INDEX sans clause USING vous donne toujours un B-tree. Ce choix par défaut est le bon la plupart du temps — ce guide traite de la reconnaissance des cas où il ne l'est pas, et des trois fonctionnalités d'index (partiel, d'expression, couvrant) qui comptent plus que la méthode d'accès dans le travail quotidien.

B-tree : le choix par défaut, à juste titre

Un index B-tree prend en charge les comparaisons d'égalité et de plage (=, <, >, BETWEEN, IN), IS NULL, le préfixe LIKE 'abc%' (avec le bon collationnement ou text_pattern_ops) et — souvent oublié — le tri : un index sur (created_at) peut satisfaire ORDER BY created_at DESC LIMIT 20 sans rien trier.

Pour les B-tree multicolonnes, l'ordre des colonnes fait tout. Un index sur (customer_id, created_at) est excellent pour WHERE customer_id = ? ORDER BY created_at, mais quasiment inutile pour une requête qui filtre uniquement sur created_at. Règle empirique : les colonnes d'égalité d'abord, puis la colonne de plage ou de tri.

Quand le B-tree est le mauvais outil

Type d'indexÀ privilégier quand…À surveiller
GIN La valeur est un conteneur et vous cherchez à l'intérieur : jsonb @> '{"status":"active"}', chevauchement de tableaux &&, recherche plein texte tsvector @@ tsquery, recherche par trigrammes avec pg_trgm pour LIKE '%term%'. Plus lent à mettre à jour qu'un B-tree ; la taille de l'index peut être importante. Convient bien aux données à forte dominante de lecture.
GiST Données géométriques, types de plage (chevauchements de tstzrange), plus proche voisin ORDER BY location <-> point et contraintes d'exclusion (par ex. « pas de réservations qui se chevauchent pour la même salle »). Avec perte (lossy) pour certaines classes d'opérateurs ; généralement plus volumineux et plus lent qu'un B-tree pour une simple égalité.
BRIN Très grandes tables en ajout seul (logs, événements) où la colonne est corrélée à l'ordre physique des lignes, typiquement des horodatages. Minuscule — des mégaoctets là où un B-tree ferait des gigaoctets. Inutile si les données ne sont pas physiquement ordonnées selon cette colonne ; il ne fait que restreindre le parcours à des plages de blocs.
Hash Égalité pure sur des valeurs longues où un B-tree devient volumineux. Rarement nécessaire ; résistant aux crashs et répliqué depuis PostgreSQL 10. Égalité uniquement : pas de plages, pas de tri, pas d'application d'unicité.
SP-GiST Structures partitionnées dans l'espace : recherches par préfixe sur du texte, plages d'IP (inet), quadtrees pour des points. De niche ; comparez-le à GiST/B-tree sur vos données réelles avant de vous engager.

Index partiels : n'indexer que ce que vous interrogez

CREATE INDEX orders_pending_idx
    ON orders (created_at)
    WHERE status = 'pending';

Si 99 % de vos commandes sont terminées mais que chaque sondage demande celles en attente, un index partiel reste petit, reste chaud en cache et est peu coûteux à maintenir — les lignes terminées ne le touchent jamais. La requête doit inclure la condition WHERE (ou quelque chose dont le planificateur peut prouver qu'elle l'implique) pour que l'index soit pris en compte.

Index d'expression : correspondre à ce que la requête calcule

CREATE INDEX users_email_lower_idx ON users (lower(email));

Une requête qui filtre sur lower(email) = lower($1) ne peut pas utiliser un simple index sur email — l'index stocke les valeurs brutes, pas leur version en minuscules. L'expression dans l'index doit correspondre à l'expression dans la requête. La même technique fonctionne pour date_trunc('day', created_at), l'extraction de champ JSON et toute fonction immuable.

Index couvrants et parcours d'index seul

CREATE INDEX orders_cust_idx
    ON orders (customer_id) INCLUDE (total, created_at);

Si l'index contient toutes les colonnes dont la requête a besoin, PostgreSQL peut ignorer complètement la table — un parcours d'index seul (index-only scan). INCLUDE ajoute des colonnes de charge utile aux feuilles de l'index sans les faire partie de la clé. Deux réserves : l'index grossit, et les parcours d'index seul exigent aussi que la visibility map soit à jour — sur une table fortement modifiée et rarement nettoyée par VACUUM, vous verrez le compteur Heap Fetches grimper et une grande partie du bénéfice disparaître.

Règles pratiques

L'EXPLAIN Visualizer signale les deux signes classiques d'un index manquant ou inadapté : les parcours séquentiels qui lisent bien plus qu'ils ne renvoient, et les compteurs Rows Removed by Filter élevés.
🧯 Erreurs associées : duplicate key value violates unique constraint, no constraint matching ON CONFLICT et violations de clé étrangère — des suppressions lentes sur la table parente signifient souvent une colonne de clé étrangère non indexée.