# 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. - Published: 2026-02-15 - Updated: 2026-03-28 - Author: SharpSkill - Tags: django, django orm, python, database optimization, performance - Reading time: 14 min --- 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. > **Règle d'or de l'optimisation** > > 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. ```python # 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.title ``` Ces modèles simples permettent d'illustrer le problème N+1 de manière concrète. ```python # 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. ```python # 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. ```python # 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. ```python # 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. ```python # 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. > **select_related vs prefetch_related** > > 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. ```python # 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. ```python # 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. ## 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. ```python # 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. ```python # 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. ```python # 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. ```python # 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. ```python # 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. > **Index et écriture** > > 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. ```python # 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. ```python # 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. ```python # 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. ```python # 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. ## 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/django/django-orm-optimiser-requetes