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.

L'ORM Django offre une abstraction élégante pour interagir avec les bases de données, mais cette simplicité peut masquer des problèmes de performance critiques. Une application Django mal optimisée peut générer des centaines de requêtes là où une seule suffirait. Ce guide explore les techniques essentielles pour identifier et résoudre ces problèmes.
Avant d'optimiser, il faut mesurer. L'utilisation de django-debug-toolbar en développement permet de visualiser chaque requête SQL générée et d'identifier rapidement les goulots d'étranglement.
Comprendre le problème N+1
Le problème N+1 représente le piège le plus courant avec les ORM. Il survient lorsqu'une requête initiale récupère N objets, puis N requêtes supplémentaires sont exécutées pour accéder aux relations de chaque objet. Cette multiplication des requêtes dégrade considérablement les performances.
# models.py
from django.db import models
class Author(models.Model):
"""Modèle représentant un auteur de livres."""
name = models.CharField(max_length=200)
email = models.EmailField(unique=True)
bio = models.TextField(blank=True)
def __str__(self):
return self.name
class Book(models.Model):
"""Modèle représentant un livre avec son auteur."""
title = models.CharField(max_length=300)
author = models.ForeignKey(
Author,
on_delete=models.CASCADE,
related_name='books'
)
published_date = models.DateField()
isbn = models.CharField(max_length=13, unique=True)
def __str__(self):
return self.titleCes modèles simples permettent d'illustrer le problème N+1 de manière concrète.
# views.py - Exemple problématique
def list_books_bad(request):
"""❌ Cette vue génère N+1 requêtes."""
books = Book.objects.all() # 1 requête pour les livres
for book in books:
# Chaque accès à book.author déclenche une requête SQL
print(f"{book.title} par {book.author.name}")
# Avec 100 livres = 101 requêtes SQL !
return render(request, 'books/list.html', {'books': books})Ce code semble innocent mais génère une requête par livre pour récupérer l'auteur associé.
Résoudre N+1 avec select_related
La méthode select_related effectue une jointure SQL et récupère les données liées en une seule requête. Elle fonctionne pour les relations ForeignKey et OneToOneField.
# views.py - Solution optimisée avec select_related
def list_books_optimized(request):
"""✅ Cette vue génère une seule requête avec JOIN."""
# select_related effectue un JOIN SQL
books = Book.objects.select_related('author').all()
for book in books:
# Aucune requête supplémentaire : author est déjà chargé
print(f"{book.title} par {book.author.name}")
# Total : 1 seule requête SQL peu importe le nombre de livres
return render(request, 'books/list.html', {'books': books})La requête SQL générée utilise un LEFT OUTER JOIN pour récupérer les auteurs en même temps que les livres.
# Chaînage de select_related pour relations imbriquées
# models.py
class Publisher(models.Model):
name = models.CharField(max_length=200)
country = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
# views.py
def list_books_with_details(request):
"""Récupère livres, auteurs et éditeurs en une requête."""
books = Book.objects.select_related(
'author', # ForeignKey vers Author
'publisher' # ForeignKey vers Publisher
).all()
return render(request, 'books/list.html', {'books': books})Plusieurs relations peuvent être optimisées simultanément en les listant dans select_related.
Optimiser les relations ManyToMany avec prefetch_related
Pour les relations ManyToMany ou les relations inverses (ForeignKey depuis l'autre côté), prefetch_related effectue des requêtes séparées mais optimisées, évitant les jointures massives.
# models.py
class Tag(models.Model):
"""Tags pour catégoriser les livres."""
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag, related_name='books')La relation ManyToMany entre Book et Tag nécessite prefetch_related pour une optimisation efficace.
# views.py - Optimisation des ManyToMany
def list_books_with_tags(request):
"""✅ Récupère les livres et leurs tags efficacement."""
books = Book.objects.prefetch_related('tags').all()
for book in books:
# Les tags sont pré-chargés, pas de requête supplémentaire
tag_names = [tag.name for tag in book.tags.all()]
print(f"{book.title}: {', '.join(tag_names)}")
# Total : 2 requêtes (livres + tags) peu importe le nombre
return render(request, 'books/list.html', {'books': books})prefetch_related exécute une requête séparée pour les tags puis effectue la jointure en Python.
Utilisez select_related pour ForeignKey et OneToOne (jointure SQL). Utilisez prefetch_related pour ManyToMany et relations inverses (requêtes séparées). Les deux peuvent être combinés sur le même QuerySet.
Personnaliser les prefetch avec Prefetch
L'objet Prefetch permet de contrôler finement les données pré-chargées : filtrage, tri, et même limitation du nombre de résultats.
# views.py
from django.db.models import Prefetch
def list_authors_with_recent_books(request):
"""Récupère les auteurs avec uniquement leurs livres récents."""
# Prefetch personnalisé : seulement les livres de 2025+
recent_books_prefetch = Prefetch(
'books',
queryset=Book.objects.filter(
published_date__year__gte=2025
).order_by('-published_date'),
to_attr='recent_books' # Stocké dans un attribut personnalisé
)
authors = Author.objects.prefetch_related(
recent_books_prefetch
).all()
for author in authors:
# Accès via l'attribut personnalisé
for book in author.recent_books:
print(f"{author.name}: {book.title}")
return render(request, 'authors/list.html', {'authors': authors})L'attribut to_attr stocke les résultats dans une liste Python plutôt que dans le manager habituel.
# Combinaison avancée : select_related + Prefetch
def list_authors_complete(request):
"""Exemple complet d'optimisation multi-niveaux."""
authors = Author.objects.prefetch_related(
Prefetch(
'books',
queryset=Book.objects.select_related(
'publisher' # Optimise aussi l'éditeur de chaque livre
).prefetch_related(
'tags' # Et les tags de chaque livre
).filter(published_date__year=2026)
)
).all()
return render(request, 'authors/complete.html', {'authors': authors})Cette approche réduit drastiquement le nombre de requêtes pour des structures de données complexes.
Prêt à réussir tes entretiens Django ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Utiliser only() et defer() pour limiter les colonnes
Par défaut, Django récupère toutes les colonnes d'une table. Pour les modèles avec de nombreux champs ou des champs volumineux, limiter les colonnes améliore les performances.
# views.py
def list_books_minimal(request):
"""Récupère uniquement les colonnes nécessaires."""
# only() spécifie les colonnes à inclure
books = Book.objects.only(
'id',
'title',
'published_date'
).select_related('author')
# Attention : accéder à un champ non inclus déclenche une requête
for book in books:
print(book.title) # OK, inclus
# print(book.isbn) # Déclencherait une requête supplémentaire
return render(request, 'books/list.html', {'books': books})La méthode only() crée un objet "différé" qui ne charge que les colonnes spécifiées.
# defer() pour exclure des colonnes spécifiques
def list_authors_without_bio(request):
"""Exclut les champs volumineux rarement utilisés."""
# defer() exclut les colonnes spécifiées
authors = Author.objects.defer(
'bio' # Le champ TextField n'est pas chargé
).all()
for author in authors:
print(author.name) # OK
print(author.email) # OK
# author.bio chargerait le champ à la demande
return render(request, 'authors/list.html', {'authors': authors})defer() représente l'inverse de only() : les colonnes listées ne sont pas chargées initialement.
Optimiser avec values() et values_list()
Quand seules certaines valeurs sont nécessaires sans les objets modèles complets, values() et values_list() retournent des dictionnaires ou tuples plus légers.
# views.py
def get_book_titles(request):
"""Récupère uniquement les titres sous forme de liste."""
# values_list retourne des tuples
titles = Book.objects.values_list('title', flat=True)
# Résultat : ['Livre 1', 'Livre 2', ...]
# values retourne des dictionnaires
book_data = Book.objects.values('title', 'published_date')
# Résultat : [{'title': 'Livre 1', 'published_date': ...}, ...]
return render(request, 'books/titles.html', {'titles': titles})Ces méthodes évitent l'instanciation des objets modèles, réduisant la consommation mémoire.
# Combinaison avec agrégations
from django.db.models import Count, Avg
def get_author_statistics(request):
"""Statistiques par auteur sans charger les objets."""
stats = Author.objects.values('name').annotate(
book_count=Count('books'),
avg_year=Avg('books__published_date__year')
).order_by('-book_count')
# Résultat : [{'name': 'Auteur', 'book_count': 5, 'avg_year': 2024}, ...]
return render(request, 'authors/stats.html', {'stats': stats})L'annotation permet d'effectuer des calculs directement en base de données.
Créer des index pour accélérer les requêtes
Les index de base de données accélèrent drastiquement les recherches. Django permet de les définir directement dans les modèles.
# models.py
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=300, db_index=True)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
published_date = models.DateField()
isbn = models.CharField(max_length=13, unique=True)
status = models.CharField(max_length=20, default='available')
class Meta:
# Index composites pour les requêtes fréquentes
indexes = [
# Index sur date de publication (tri fréquent)
models.Index(
fields=['published_date'],
name='book_pub_date_idx'
),
# Index composite pour filtrage auteur + statut
models.Index(
fields=['author', 'status'],
name='book_author_status_idx'
),
# Index partiel : seulement les livres disponibles
models.Index(
fields=['published_date'],
name='book_available_idx',
condition=models.Q(status='available')
),
]
# Ordre par défaut utilisant l'index
ordering = ['-published_date']Ces index améliorent les performances des requêtes filtrant sur ces colonnes.
Les index accélèrent les lectures mais ralentissent légèrement les écritures (INSERT, UPDATE). Créez des index uniquement sur les colonnes fréquemment utilisées dans WHERE, ORDER BY ou JOIN.
Utiliser les requêtes brutes quand nécessaire
Pour des requêtes complexes ou des optimisations spécifiques à la base de données, les requêtes SQL brutes offrent un contrôle total.
# views.py
from django.db import connection
def get_books_with_raw_sql(request):
"""Requête brute pour cas spéciaux."""
# Méthode 1 : raw() pour récupérer des objets modèles
books = Book.objects.raw('''
SELECT b.*, a.name as author_name
FROM library_book b
INNER JOIN library_author a ON b.author_id = a.id
WHERE b.published_date > %s
ORDER BY b.published_date DESC
''', ['2025-01-01'])
return render(request, 'books/list.html', {'books': books})
def execute_custom_query(request):
"""Exécution directe pour requêtes non-SELECT."""
with connection.cursor() as cursor:
# Requête avec agrégation complexe
cursor.execute('''
SELECT
a.name,
COUNT(b.id) as book_count,
AVG(EXTRACT(YEAR FROM b.published_date)) as avg_year
FROM library_author a
LEFT JOIN library_book b ON b.author_id = a.id
GROUP BY a.id, a.name
HAVING COUNT(b.id) > 2
ORDER BY book_count DESC
''')
results = cursor.fetchall()
return render(request, 'stats.html', {'results': results})Les requêtes brutes contournent l'ORM mais perdent la portabilité entre bases de données.
Analyser les requêtes avec django-debug-toolbar
L'outil django-debug-toolbar permet de visualiser toutes les requêtes SQL générées par une vue Django.
# settings.py - Configuration debug toolbar
INSTALLED_APPS = [
# ... autres apps
'debug_toolbar',
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
# ... autres middlewares
]
# Afficher la toolbar pour les requêtes locales
INTERNAL_IPS = ['127.0.0.1']
DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.sql.SQLPanel', # Requêtes SQL
'debug_toolbar.panels.timer.TimerPanel', # Temps d'exécution
'debug_toolbar.panels.cache.CachePanel', # Cache
]Cette configuration active les panneaux les plus utiles pour l'optimisation.
# Logging des requêtes SQL en développement
# settings.py
LOGGING = {
'version': 1,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'level': 'DEBUG',
'handlers': ['console'],
},
},
}Ce logging affiche chaque requête SQL dans la console, utile pour identifier les problèmes N+1.
Mise en cache des QuerySets
Pour les données fréquemment accédées et rarement modifiées, la mise en cache évite des requêtes répétitives.
# views.py
from django.core.cache import cache
from django.views.decorators.cache import cache_page
def list_featured_books(request):
"""Récupère les livres mis en avant avec cache."""
cache_key = 'featured_books_list'
# Tente de récupérer depuis le cache
books = cache.get(cache_key)
if books is None:
# Cache miss : exécute la requête
books = list(
Book.objects.select_related('author')
.filter(featured=True)
.order_by('-published_date')[:10]
)
# Stocke en cache pour 5 minutes
cache.set(cache_key, books, timeout=300)
return render(request, 'books/featured.html', {'books': books})
# Décorateur pour cacher la vue entière
@cache_page(60 * 15) # Cache 15 minutes
def list_all_tags(request):
"""Liste tous les tags (données rarement modifiées)."""
tags = Tag.objects.annotate(
book_count=Count('books')
).order_by('-book_count')
return render(request, 'tags/list.html', {'tags': tags})Le cache réduit la charge sur la base de données pour les données statiques.
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Conclusion
L'optimisation des requêtes Django ORM repose sur quelques principes fondamentaux :
✅ Mesurer avant d'optimiser : utilisez django-debug-toolbar pour identifier les problèmes réels
✅ Éliminer le problème N+1 : select_related pour ForeignKey, prefetch_related pour ManyToMany
✅ Limiter les données : only(), defer(), values() pour ne charger que le nécessaire
✅ Indexer intelligemment : créez des index sur les colonnes fréquemment filtrées ou triées
✅ Mettre en cache : les données rarement modifiées bénéficient du cache Django
✅ Requêtes brutes : en dernier recours pour les optimisations spécifiques
Ces techniques, appliquées méthodiquement, transforment une application Django lente en un système performant capable de gérer des volumes de données importants. L'ORM Django reste un outil puissant quand ses subtilités sont maîtrisées.
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tags
Partager
Articles similaires

Django 5.2 : Middleware Personnalisé et Gestion des Signaux pour les Entretiens Techniques
Guide complet sur les middleware personnalisés et les signaux dans Django 5.2. Exemples pratiques de middleware de logging, middleware asynchrone, signaux pre_save/post_save et questions fréquentes en entretien.

Questions d'entretien Django et Python : Top 25 en 2026
Les 25 questions d'entretien Django et Python les plus posées. ORM, vues, middleware, DRF, signaux et optimisation avec réponses détaillées et exemples de code.

Django 5 : Créer une API REST avec Django REST Framework
Guide complet pour créer une API REST professionnelle avec Django 5 et DRF. Serializers, ViewSets, authentification JWT et bonnes pratiques expliqués.