Was ist das N+1-Query-Problem?
Stellen wir uns eine Anwendung vor, die eine Liste von Nutzern und deren Bestellungen anzeigt. Um die Nutzer abzurufen, führt die Anwendung eine Datenbankabfrage aus. Anschließend lädt sie jedoch die Bestellungen jedes Nutzers einzeln.
Bei 100 Nutzern ergibt sich folgendes Szenario:
- 1 Abfrage, um alle Nutzer abzurufen.
- 100 weitere Abfragen, um die Bestellungen der einzelnen Nutzer zu laden.
Das sind insgesamt 101 Datenbankabfragen statt einer kleinen Anzahl optimierter Abfragen, mit denen sich die benötigten Daten effizienter abrufen ließen.
Das Problem heißt N+1, weil die Anwendung zunächst eine Abfrage ausführt und anschließend N weitere Abfragen – in der Regel eine für jeden Datensatz im Ergebnis.
Was passiert, wenn die Anwendung 1.000 Nutzer hat? Statt einer Abfrage kann sie dann 1.001 ausführen. Jede einzelne Abfrage mag schnell sein, doch in ihrer Gesamtheit verlängern sie die Ladezeit und belasten sowohl die Datenbank als auch die Kommunikation zwischen Anwendung und Datenbankserver unnötig.
Wie entsteht das N+1-Problem in Django?
Django verwendet ein ORM (Object-Relational Mapping), mit dem Entwickler über Python-Objekte auf Datenbankeinträge zugreifen können, ohne für jede Operation SQL-Abfragen schreiben zu müssen.
Das ORM vereinfacht die Entwicklung. Allerdings ist nicht immer auf den ersten Blick erkennbar, wie viele Datenbankabfragen tatsächlich ausgeführt werden.
Schauen wir uns ein vereinfachtes Beispiel an. Wir gehen davon aus, dass es ein Modell User und ein Modell Order gibt. Jede Bestellung gehört zu einem bestimmten Nutzer, und die Beziehung ist mit related_name="orders" konfiguriert.
users = User.objects.all()
for user in users:
orders = user.orders.all()
print(user.username, list(orders))
Der Code sieht einfach aus. Zunächst rufen wir die Nutzer ab, anschließend laden wir die Bestellungen für jeden einzelnen Nutzer.
Das Problem: user.orders.all() kann für jeden Nutzer eine separate Datenbankabfrage auslösen. Enthält die Liste 100 Nutzer, führt die Anwendung möglicherweise 101 Abfragen aus.
Dieses Verhalten kann in Templates, Serializern oder verschachtelten Schleifen leicht unbemerkt bleiben. Entwickler arbeiten mit Objekten und deren Beziehungen, während die Datenbank im Hintergrund wiederholt dieselben Operationen ausführt.
So lässt sich das Problem beheben: select_related() und prefetch_related()
Django bietet zwei wichtige Methoden zur Optimierung des Ladens zusammengehöriger Objekte: select_related() und prefetch_related().
Beide helfen dabei, unnötige Datenbankabfragen zu reduzieren. Sie funktionieren jedoch unterschiedlich. Welche Methode die richtige ist, hängt von der Art der Beziehung zwischen den Modellen ab.
1. select_related(): Wenn Datensätze über einen Fremdschlüssel verbunden sind
Die Methode select_related() wird hauptsächlich bei Beziehungen über ForeignKey und OneToOneField verwendet. Django nutzt dabei einen SQL JOIN, um die zugehörigen Daten innerhalb einer einzigen Abfrage abzurufen.
Betrachten wir den umgekehrten Fall: Wir möchten eine Liste von Bestellungen anzeigen und zu jeder Bestellung den Namen des Nutzers ausgeben, der sie aufgegeben hat.
Der nicht optimierte Code könnte so aussehen:
orders = Order.objects.all()
for order in orders:
print(order.user.username)
Bei 100 Bestellungen führt Django möglicherweise eine Abfrage für die Bestellungen und weitere 100 Abfragen für die jeweiligen Nutzer aus.
Die optimierte Version sieht so aus:
orders = Order.objects.select_related("user")
for order in orders:
print(order.user.username)
Django lädt die Bestellungen und die zugehörigen Nutzer gemeinsam. Anstatt den Nutzer für jede Bestellung separat abzurufen, kann eine einzige SQL-Abfrage ausreichen.
Wann sollte man select_related() verwenden?
Wenn ein zugehöriges Objekt über eine Beziehung geladen werden soll, bei der jeder Datensatz höchstens ein zugehöriges Objekt hat. Beispiele sind der Nutzer einer Bestellung oder das Profil eines einzelnen Nutzers.
2. prefetch_related(): Wenn mehrere zugehörige Datensätze benötigt werden
Was aber, wenn wir Nutzer und alle ihre Bestellungen anzeigen möchten? Ein Nutzer kann keine, eine oder Dutzende Bestellungen haben. In diesem Fall hilft prefetch_related().
users = User.objects.prefetch_related("orders")
for user in users:
print(user.username)
for order in user.orders.all():
print(order.id)
Django lädt zunächst die Nutzer und führt anschließend eine separate Abfrage aus, um die zugehörigen Bestellungen abzurufen. Danach ordnet es die Bestellungen den entsprechenden Nutzern in Python zu.
In einem typischen Beispiel reichen damit zwei Datenbankabfragen statt 101: eine für die Nutzer und eine für ihre Bestellungen.
prefetch_related() eignet sich besonders für Beziehungen über ManyToManyField sowie für umgekehrte ForeignKey-Beziehungen, bei denen einem Objekt mehrere andere Objekte zugeordnet sein können.
Im Gegensatz zu select_related() verwendet prefetch_related() separate Abfragen und führt die Ergebnisse anschließend in Python zusammen. Das macht die Methode auch für Situationen geeignet, in denen ein gewöhnlicher SQL JOIN zu wiederholten Daten in der Ergebnismenge führen würde.
Wichtig ist, dass weniger Abfragen nicht automatisch in jeder Situation besser sind. Das Ziel besteht darin, unnötige Arbeit zu vermeiden und eine Methode zu wählen, die zu den Daten und zum jeweiligen Anwendungsfall passt.

Lazy Loading: Warum wird eine Abfrage erst später ausgeführt?
Ein Grund dafür, dass das N+1-Problem leicht übersehen wird, ist das sogenannte Lazy Loading. Dabei werden Daten erst dann geladen, wenn sie tatsächlich benötigt werden.
Django-QuerySets werden in der Regel erst ausgewertet, wenn die Anwendung ihre Ergebnisse benötigt. Das passiert beispielsweise beim Durchlaufen eines QuerySets, bei der Umwandlung in eine Liste oder bei bestimmten anderen Operationen.
Auch über Fremdschlüssel verknüpfte Objekte werden häufig erst dann geladen, wenn der Code auf sie zugreift. Greift eine Anwendung innerhalb einer Schleife wiederholt auf Beziehungen zu, die nicht im Voraus geladen wurden, kann sie ungewollt zusätzliche Abfragen erzeugen.
Deshalb muss das Problem nicht unbedingt in der Zeile sichtbar sein, in der die Nutzerliste abgerufen wird. Es kann erst später auftreten, etwa in einem Template, das den Namen eines Nutzers, seine Bestellungen oder andere zugehörige Informationen anzeigt.
Lazy Loading ist an sich kein Fehler. Es hilft dabei, nur die Daten abzurufen, die tatsächlich benötigt werden. Problematisch wird es, wenn das verzögerte Laden wiederholt stattfindet und dadurch unnötige Datenbankoperationen entstehen.
Wie lässt sich das N+1-Query-Problem erkennen?
Der erste Schritt besteht darin, herauszufinden, wie viele Datenbankabfragen die Anwendung tatsächlich ausführt. Ein Blick auf den Python-Code allein reicht dafür oft nicht aus.
Es gibt mehrere Möglichkeiten, das Problem zu erkennen.
Django Debug Toolbar ist ein nützliches Werkzeug während der lokalen Entwicklung. Es zeigt die beim Laden einer Seite ausgeführten SQL-Abfragen sowie deren Anzahl und Ausführungszeit an. Wenn dieselbe oder eine sehr ähnliche Abfrage Dutzende oder Hunderte Male erscheint, kann das ein Hinweis auf ein Problem sein.
SQL-Query-Logging ermöglicht eine detailliertere Analyse. Entwickler können nachvollziehen, welche Abfragen ausgeführt werden, wie häufig sie vorkommen und wie lange sie dauern. In der Produktionsumgebung sollte das Logging sorgfältig konfiguriert werden, damit es keine unnötige zusätzliche Belastung verursacht oder sensible Daten offenlegt.
Tests für die Anzahl der Abfragen helfen dabei, Performance-Regressionen vor dem Deployment zu erkennen. Django stellt beispielsweise assertNumQueries() zur Verfügung. Damit lässt sich in Tests überprüfen, ob eine bestimmte Operation unerwartet viele Datenbankabfragen ausführt.
Das Profiling langsamer Seiten und API-Endpunkte hilft dabei, die Bereiche zu identifizieren, in denen Optimierungen den größten Nutzen bringen. Bei APIs lohnt sich auch ein Blick auf die Serializer, da diese beim Verarbeiten einzelner Objekte auf zugehörige Datensätze zugreifen und dadurch zusätzliche Abfragen auslösen können.
Nicht jede wiederholte Abfrage bedeutet automatisch ein N+1-Problem, und nicht jeder langsame Endpunkt ist davon betroffen. Entscheidend ist, die Anzahl der Abfragen im Verhältnis zur verarbeiteten Datenmenge zu betrachten und zu prüfen, wie viel Zeit sie tatsächlich beanspruchen.
Wie lässt sich das N+1-Problem vermeiden?
Die beste Optimierung ist diejenige, die nicht erst notwendig wird, wenn sich Nutzer über eine langsame Anwendung beschweren. Mit einigen bewährten Vorgehensweisen lässt sich das Risiko unnötiger Abfragen deutlich reduzieren.
- Berücksichtigen Sie Datenbankbeziehungen bereits bei der Planung einer Funktion. Wenn eine Seite sowohl Bestellungen als auch die Namen der zugehörigen Nutzer anzeigen soll, sollte bereits im Voraus geplant werden, wie die benötigten Daten effizient geladen werden.
- Verwenden Sie select_related() und prefetch_related(), wenn es sinnvoll ist. Gehen Sie nicht davon aus, dass das ORM automatisch die effizienteste Methode zum Laden aller benötigten Daten auswählt. Vermeiden Sie aber auch den unüberlegten Einsatz dieser Methoden. Große Mengen unnötiger Daten zu laden, kann ebenfalls ineffizient sein.
- Überprüfen Sie Templates und Serializer. Das N+1-Problem muss nicht direkt in einer View entstehen. Es kann auch im Code auftreten, der einzelne Objekte beim Rendern einer Seite oder beim Erstellen einer API-Antwort verarbeitet.
- Testen Sie mit realistischen Datenmengen. Bei fünf Nutzern ist der Unterschied zwischen sechs und zwei Abfragen möglicherweise kaum spürbar. Bei Hunderten oder Tausenden von Datensätzen kann er jedoch erheblich sein. Neben der Anzahl der Abfragen sollten auch deren Ausführungszeit und das Volumen der geladenen Daten berücksichtigt werden.
- Überwachen Sie die Performance kontinuierlich. Abfragetests, Profiling und die regelmäßige Analyse langsamer Endpunkte helfen dabei, Probleme frühzeitig zu erkennen, bevor sie sich bei höherer Auslastung bemerkbar machen.
Fazit: Weniger Abfragen können eine Anwendung beschleunigen
Das N+1-Query-Problem zeigt anschaulich, wie ein scheinbar kleines Detail im Code allmählich zu einem Performance-Engpass werden kann. Eine Anwendung kann zunächst einwandfrei funktionieren und dennoch mit zunehmender Datenmenge immer mehr unnötige Arbeit verrichten.
In Django reicht es häufig aus, zu verstehen, wann Daten geladen werden, und select_related() oder prefetch_related() gezielt einzusetzen. Statt einer anfänglichen Abfrage und Dutzenden oder Hunderten zusätzlicher Abfragen kann die Anwendung die benötigten Daten so mit wenigen, effizienteren Operationen abrufen.
Bei der Datenbankoptimierung geht es nicht nur darum, jede einzelne Abfrage schneller zu machen. Ebenso wichtig ist es, die Anzahl der Abfragen zu reduzieren, die eine Anwendung überhaupt benötigt.