Django ORM Performance-Optimierung:
N+1, select_related und Indexes
Das N+1-Problem ist einer der häufigsten Performance-Killer in Django-Projekten — und gleichzeitig einer der am einfachsten zu behebenden. Ich begegne ihm regelmäßig in Code-Reviews und bei Projekten, die ich neu übernehme. In diesem Artikel zeige ich dir, wie du es erkennst, behebst und von vornherein vermeidest.
Das N+1-Problem verstehen
Stell dir vor, du hast ein Blog-System mit Autoren und Artikeln. Du willst eine Liste aller Artikel mit dem jeweiligen Autorennamen anzeigen:
# ❌ Das N+1-Problem — ineffizient!
articles = Article.objects.all()
for article in articles:
# Für jeden Artikel wird eine separate DB-Query ausgeführt!
print(article.author.name)
Was passiert hier? Django lädt zunächst alle Artikel mit einer Query. Für jeden Artikel wird dann beim Zugriff auf article.author eine weitere Query ausgeführt. Bei 100 Artikeln sind das also 101 Queries — 1 für die Liste, 100 für die Autoren. Das ist das N+1-Problem.
Faustregel: Wenn du in einer Schleife auf eine verwandte Relation zugreifst, hast du wahrscheinlich ein N+1-Problem.
select_related für ForeignKey und OneToOne
select_related löst das Problem durch einen SQL JOIN — statt N+1 Queries gibt es genau eine:
# ✅ Korrekt mit select_related
articles = Article.objects.select_related('author').all()
for article in articles:
# Kein DB-Zugriff mehr — author wurde bereits mitgeladen
print(article.author.name)
# Mehrere Relationen und Tiefe:
articles = Article.objects.select_related(
'author',
'author__profile',
'category'
)
select_related funktioniert für ForeignKey und OneToOneField. Es erzeugt einen SQL JOIN, was bedeutet: Es ist am effizientesten wenn die verwandten Objekte immer benötigt werden und die Datenmenge überschaubar ist.
prefetch_related für ManyToMany und reverse ForeignKey
Für ManyToManyField und umgekehrte ForeignKey-Relationen ist prefetch_related die richtige Wahl:
# Artikel mit Tags (ManyToMany) und Kommentaren (reverse FK)
articles = Article.objects.prefetch_related(
'tags',
'comments',
'comments__author'
)
for article in articles:
# Alle Tags und Kommentare bereits im Cache
tags = article.tags.all()
comments = article.comments.all()
prefetch_related führt separate Queries aus (eine pro Relation) und verknüpft die Ergebnisse in Python. Mit Prefetch lässt sich die Teilquery noch weiter einschränken:
from django.db.models import Prefetch
# Nur genehmigte Kommentare laden
articles = Article.objects.prefetch_related(
Prefetch(
'comments',
queryset=Comment.objects.filter(approved=True).select_related('author'),
to_attr='approved_comments'
)
)
for article in articles:
# Direkter Zugriff auf das gefilterte Ergebnis
print(article.approved_comments)
only() und defer() — nur laden was gebraucht wird
Wenn du nur bestimmte Felder benötigst, musst du nicht die gesamte Zeile laden:
# Nur id, title und created_at laden
articles = Article.objects.only('id', 'title', 'created_at')
# Oder umgekehrt: alles AUSSER dem body-Feld (ggf. sehr groß)
articles = Article.objects.defer('body', 'raw_html')
Vorsicht: Wenn du auf ein deferred field zugreifst, wird doch eine Query ausgeführt. Deshalb am besten mit values() oder values_list() arbeiten wenn du wirklich nur Rohdaten brauchst:
# Sehr effizient: gibt Dictionaries zurück
Article.objects.values('id', 'title')
# Noch effizienter: gibt Tupel zurück
Article.objects.values_list('id', 'title', flat=False)
# Einzelnes Feld als flache Liste
ids = Article.objects.values_list('id', flat=True)
Datenbankindizes richtig einsetzen
Selbst perfekte Queries können langsam sein wenn die richtigen Indizes fehlen. Django macht es einfach, Indizes direkt im Model zu definieren:
class Article(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(User, on_delete=models.CASCADE)
status = models.CharField(max_length=20)
created_at = models.DateTimeField(auto_now_add=True)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
class Meta:
indexes = [
# Einfacher Index für häufige Filterung
models.Index(fields=['status']),
# Zusammengesetzter Index für kombinierte Filter
models.Index(fields=['status', 'created_at']),
# Index mit Name für spätere Migration
models.Index(fields=['author', 'category'], name='article_author_cat_idx'),
]
ForeignKey-Felder bekommen automatisch einen Index. Setze Indizes auf Felder die häufig in filter() , order_by() oder JOIN-Bedingungen vorkommen.
Queries debuggen mit django-debug-toolbar
Ohne Sichtbarkeit in die tatsächlichen Queries tappt man im Dunkeln. Installiere django-debug-toolbar für die Entwicklung:
# settings.py
INSTALLED_APPS += ['debug_toolbar']
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
INTERNAL_IPS = ['127.0.0.1']
Alternativ kannst du Queries direkt in der Django-Shell analysieren:
from django.db import connection, reset_queries
from django.conf import settings
settings.DEBUG = True
reset_queries()
# Dein Code hier:
articles = list(Article.objects.select_related('author').all())
print(len(connection.queries), "Queries")
for q in connection.queries:
print(q['sql'])
Fazit
Die meisten Django Performance-Probleme lassen sich mit drei einfachen Regeln lösen:
- Immer
select_related()für ForeignKey/OneToOne verwenden wenn die verwandten Daten gebraucht werden prefetch_related()für ManyToMany und reverse Relations- Indizes auf Felder setzen die häufig gefiltert oder sortiert werden
Und vor allem: Queries messen, nicht raten. Django Debug Toolbar oder connection.queries zeigen dir genau was passiert.