Django et PostgreSQL en 2026 : indexation, recherche plein texte et questions d'entretien
Guide pratique de l'optimisation Django PostgreSQL : index B-tree, partiels et couvrants, recherche plein texte avec SearchVector et GIN, plus les questions d'entretien 2026.

L'optimisation Django PostgreSQL fait la différence entre une application qui passe le cap des premiers milliers d'utilisateurs et une autre qui s'effondre à chaque vue de liste. PostgreSQL embarque un moteur d'indexation et un sous-système de recherche plein texte que la plupart des projets Django laissent inexploités, gaspillant des performances de requête qu'il ne coûte rien de récupérer. Ce guide couvre les patterns d'indexation, les outils de recherche de django.contrib.postgres et les questions d'entretien sur les bases de données qui distinguent les développeurs Django intermédiaires des profils seniors en 2026.
Chaque index accélère les lectures, mais ralentit les écritures et consomme de l'espace disque. Il faut lancer EXPLAIN (ANALYZE, BUFFERS) sur la requête réelle, ajouter l'index, puis comparer le plan. Un index que le planificateur ne choisit jamais représente une surcharge pure à chaque INSERT et UPDATE.
Les fondamentaux de l'indexation PostgreSQL dans Django
Par défaut, PostgreSQL crée un index B-tree uniquement sur les clés primaires et les colonnes définies avec unique=True. Toute autre colonne filtrée ou triée déclenche un parcours séquentiel tant qu'aucun index n'existe. Django expose les index via Meta.indexes, une approche préférable à l'ancien db_index=True car elle regroupe au même endroit les index composites, partiels et couvrants.
L'ordre des colonnes dans un index composite n'a rien de cosmétique. Un B-tree ne peut exploiter un index pour une requête que si le filtre utilise un préfixe de tête des colonnes indexées, une règle détaillée sur Use The Index, Luke. Un index sur (status, created_at) répond à WHERE status = ? ORDER BY created_at, mais ce même index ne peut accélérer une requête qui filtre uniquement sur created_at.
# models.py
from django.db import models
class Order(models.Model):
customer_email = models.EmailField()
status = models.CharField(max_length=20)
total = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
# Index B-tree mono-colonne pour recherches exactes et par plage sur status
models.Index(fields=["status"]),
# Index composite : répond à WHERE status = ? ORDER BY created_at DESC
# La colonne de tête (status) doit figurer dans le filtre pour être utilisée
models.Index(fields=["status", "-created_at"]),
]L'index composite transforme une requête de tableau de bord comme « dernières commandes en attente » en un simple parcours d'index, au lieu d'un filtrage suivi d'un tri sur toute la table.
Index partiels et couvrants pour des requêtes ciblées
Un index partiel ne couvre que les lignes correspondant à une condition, ce qui le maintient compact et en mémoire. Lorsque la plupart des lignes partagent une même valeur et que les requêtes ne visent que la minorité, un index partiel revient nettement moins cher qu'un index complet. Un index couvrant va plus loin : en ajoutant des colonnes non clés avec include, PostgreSQL répond à la requête directement depuis l'index, sans toucher au tas de la table, un mode d'accès appelé index-only scan.
# models.py
from django.db import models
from django.db.models import Q
class Order(models.Model):
# champs définis ci-dessus
class Meta:
indexes = [
# Index partiel : n'indexe que les lignes en attente, ignore l'historique terminé
models.Index(
fields=["created_at"],
name="pending_orders_idx",
condition=Q(status="pending"),
),
# Index couvrant : total est lu directement depuis l'index (index-only scan)
models.Index(
fields=["status"],
include=["total"],
name="status_total_covering_idx",
),
]Sur une table de commandes où 2 % des lignes sont en attente, l'index partiel peut être cinquante fois plus petit qu'un index complet sur la même colonne, et le planificateur le garde chaud dans le cache tampon. La clause include est documentée dans PostgreSQL index-only scans et constitue le gain le plus souvent négligé dans les applications Django qui agrègent une colonne numérique derrière un filtre de statut.
Recherche plein texte Django avec PostgreSQL
Recourir à title__icontains=query paraît pratique, mais force un parcours séquentiel et ne peut classer les résultats par pertinence. La recherche plein texte de Django encapsule les types natifs tsvector et tsquery de PostgreSQL via SearchVector, SearchQuery et SearchRank. Un vecteur de recherche normalise le texte en lexèmes, supprime les mots vides et ramène les mots à leur racine, si bien qu'une recherche sur « running » correspond aussi à « run » et « ran ».
# views.py
from django.contrib.postgres.search import (
SearchVector, SearchQuery, SearchRank,
)
from .models import Article
def search_articles(query_text):
query = SearchQuery(query_text, config="english")
# Le poids A classe les correspondances de title avant celles du body (poids B)
vector = (
SearchVector("title", weight="A")
+ SearchVector("body", weight="B")
)
return (
Article.objects.annotate(rank=SearchRank(vector, query))
.filter(rank__gte=0.1)
.order_by("-rank")
)Le paramètre weight attribue des niveaux de priorité de A (le plus élevé) à D. Une correspondance dans le titre passe avant une correspondance dans le corps, même lorsque les deux contiennent le terme recherché, ce qui correspond au comportement attendu par les utilisateurs. L'API complète et le modèle de pondération à quatre niveaux sont détaillés dans la référence de la recherche plein texte Django.
Index GIN et SearchVectorField pour une recherche rapide
La requête précédente recalcule le vecteur de recherche pour chaque ligne à chaque requête, ce qui convient pour quelques milliers de lignes mais devient inacceptable au million. Le pattern de production stocke le vecteur dans un SearchVectorField et l'indexe avec un index GIN, le type d'index que PostgreSQL utilise pour les valeurs composites comme les documents plein texte et le JSONB.
# models.py
from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=255)
body = models.TextField()
# Document de recherche stocké et pré-calculé
search_vector = SearchVectorField(null=True)
class Meta:
indexes = [
# GIN transforme les recherches plein texte en parcours d'index logarithmiques
GinIndex(fields=["search_vector"]),
]Le champ stocké doit rester synchronisé avec title et body. Un signal post_save qui écrit le vecteur via .update() évite de déclencher le signal de manière récursive, car QuerySet.update() n'émet pas de signaux de sauvegarde.
# signals.py
from django.contrib.postgres.search import SearchVector
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Article
@receiver(post_save, sender=Article)
def sync_search_vector(sender, instance, **kwargs):
Article.objects.filter(pk=instance.pk).update(
search_vector=SearchVector("title", weight="A")
+ SearchVector("body", weight="B"),
)Pour les équipes sous Django 5.0 ou plus récent, un GeneratedField calculé par la base ou un trigger PostgreSQL supprime totalement l'UPDATE supplémentaire en maintenant la colonne au sein de la base. Quel que soit le mécanisme qui la remplit, la requête filtre ensuite directement le champ indexé, et l'index GIN transforme ce qui était un parcours complet de table en une recherche logarithmique.
Les index GIN accélèrent les lectures au prix réel des écritures, car chaque insertion touche de nombreuses entrées d'index. Sur les tables à fort volume d'écriture, il faut régler le paramètre de stockage fastupdate et surveiller la taille de la liste d'attente, sous peine de voir les écritures plein texte bloquées derrière la maintenance de l'index.
Choisir le bon type d'index PostgreSQL
Le B-tree est le choix par défaut le plus adapté, mais PostgreSQL propose plusieurs types d'index via django.contrib.postgres.indexes, et opter pour le mauvais gaspille de l'espace disque sans améliorer la moindre requête. Le choix dépend de la forme des données et de l'opérateur utilisé par la requête, comme le recense la documentation des types d'index PostgreSQL.
| Type d'index | Idéal pour | Classe Django |
|------------|----------|--------------|
| B-tree | Égalité et plage sur colonnes scalaires | models.Index |
| GIN | Recherche plein texte, JSONB, appartenance à un tableau | GinIndex |
| BRIN | Tables énormes en ajout seul, ordonnées par une colonne | BrinIndex |
| Hash | Recherches par égalité seule sur colonnes volumineuses | HashIndex |
Le BRIN mérite une attention particulière pour les données de séries temporelles. Sur une table de dizaines de millions de lignes insérées dans l'ordre chronologique, un BrinIndex sur created_at occupe quelques kilo-octets là où un B-tree en réclamerait des centaines de méga-octets, car le BRIN ne stocke que les valeurs minimale et maximale par plage de blocs. En contrepartie, le BRIN n'aide que lorsque l'ordre physique des lignes suit la colonne indexée, ce qui se vérifie naturellement pour les journaux en ajout seul et les tables d'événements.
# models.py
from django.contrib.postgres.indexes import BrinIndex
from django.db import models
class Event(models.Model):
payload = models.JSONField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
# Index minuscule pour parcours par plage sur une table en ajout seul, ordonnée dans le temps
BrinIndex(fields=["created_at"]),
]Adapter le type d'index au pattern d'accès est un thème récurrent des entretiens seniors, car cela prouve qu'un candidat raisonne sur l'organisation du stockage plutôt que d'ajouter par réflexe des B-tree partout.
Prêt à réussir tes entretiens Django ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Questions d'entretien Django sur les bases de données en 2026
Les questions sur les bases de données dominent les entretiens Django seniors, car maîtriser l'ORM n'implique pas de comprendre ce que l'ORM produit réellement. Les questions ci-dessous reflètent ce que les jurys de recrutement sondent effectivement en 2026, et elles se complètent naturellement avec l'optimisation au niveau des requêtes abordée dans le guide d'optimisation des requêtes de l'ORM Django.
Quand un index composite sur (a, b) n'aide-t-il pas une requête ? Lorsque la requête ne filtre pas sur a. Un B-tree est ordonné d'abord par sa colonne de tête, donc un filtre sur b seul ne peut exploiter l'index. C'est la règle du préfixe le plus à gauche, et les candidats qui l'expliquent démontrent qu'ils comprennent la structure d'un index plutôt que des recettes apprises par cœur.
Quelle est la différence entre select_related et prefetch_related ? select_related effectue une jointure SQL et convient aux relations de clé étrangère et un-à-un en une seule requête. prefetch_related lance une seconde requête et assemble les résultats en Python, ce qui est nécessaire pour les relations plusieurs-à-plusieurs et les clés étrangères inverses. Choisir le mauvais des deux fait manquer l'optimisation ou déclenche une seconde requête inutile.
En quoi icontains diffère-t-il de la recherche plein texte au niveau de la base ? icontains se compile en ILIKE '%term%', qui ne peut utiliser un index B-tree standard et parcourt chaque ligne. La recherche plein texte s'appuie sur un tsvector pré-calculé adossé à un index GIN et classe les résultats par pertinence. Cette distinction est un signal fiable qu'un candidat a dépassé le stade des jeux de données jouets.
Pourquoi count() peut-il être lent sur une grande table, et quelles sont les alternatives ? PostgreSQL ne conserve aucun décompte de lignes en cache pour une table, donc COUNT(*) parcourt chaque ligne visible sous MVCC. Pour un décompte approximatif, interroger pg_class.reltuples renvoie une estimation instantanée, et pour la pagination, la pagination par clé (keyset) sur une colonne indexée évite tout décompte. Une pratique structurée de ces patterns est disponible dans le module d'entretien sur le cache Django, où les compteurs adossés au cache reviennent régulièrement, ainsi que sur l'ensemble du parcours d'apprentissage Django.
Qu'est-ce que EXPLAIN ANALYZE révèle que EXPLAIN seul ne montre pas ? EXPLAIN affiche le coût estimé par le planificateur ; EXPLAIN ANALYZE exécute réellement la requête et rapporte les temps et nombres de lignes réels. Un écart important entre les lignes estimées et réelles trahit des statistiques de table obsolètes, corrigeables avec ANALYZE, et constitue une cause fréquente du planificateur ignorant un index qui devrait manifestement s'appliquer.
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Conclusion
- Définir les index dans
Meta.indexesplutôt qu'avecdb_index=True, afin que les index composites, partiels et couvrants figurent à un seul endroit auditable. - Ordonner les colonnes des index composites selon la règle du préfixe le plus à gauche : la colonne de tête doit apparaître dans le filtre de la requête pour que l'index s'applique.
- Recourir aux index partiels lorsque les requêtes ne visent qu'une minorité de lignes, comme les commandes en attente dans une table dominée par l'historique terminé.
- Ajouter des index couvrants avec
includepour activer les index-only scans et éviter l'accès au tas sur les requêtes d'agrégation à forte lecture. - Remplacer la recherche par
icontainsparSearchVector,SearchQueryetSearchRankpour obtenir gratuitement le classement par pertinence et la racinisation. - Stocker le document de recherche dans un
SearchVectorFieldadossé à unGinIndex, maintenu synchronisé par un signal, unGeneratedFieldou un trigger de base de données, avant que la recherche plein texte n'atteigne l'échelle de production. - Toujours valider les décisions d'indexation avec
EXPLAIN (ANALYZE, BUFFERS); un index que le planificateur ne sélectionne jamais est une taxe à l'écriture sans aucun bénéfice à la lecture.
Tags
Partager
Articles similaires

Django ORM : Optimiser vos requêtes pour des performances maximales
Guide complet pour optimiser les requêtes Django ORM. select_related, prefetch_related, index, analyse N+1 et techniques avancées pour des applications performantes.

Vues asynchrones Django et ASGI en 2026 : performance et questions d’entretien
Une plongée en profondeur dans les vues asynchrones Django et ASGI en 2026 : leur fonctionnement interne, quel serveur déployer, l’ORM asynchrone et le piège SynchronousOnlyOperation, plus des questions d’entretien.

Django 6.0 en 2026 : clés primaires composites, tâches en arrière-plan et questions d'entretien
Django 6.0 en 2026 : clés primaires composites, tâches en arrière-plan natives, template partials, middleware CSP et questions d'entretien technique.