N+1-Queries: Wie 280 ms zu 12 Sekunden werden

Ein sauberer Code, der eine API zerstört
Es gibt Bugs, die man sofort sieht. Und es gibt welche, die sich jahrelang hinter lesbarem, gut strukturiertem Code verstecken – bis der erste echte Datenbankzustand sie sichtbar macht.
Das N+1-Problem gehört zur zweiten Kategorie. Es entsteht nicht durch Fehler im klassischen Sinne, sondern durch eine Eigenschaft moderner ORM-Frameworks: Sie machen Datenbankzugriffe so einfach, dass man gar nicht mehr merkt, wie viele man macht.
Ein konkretes Beispiel aus der Praxis: Ein API-Endpoint für eine Immobilienverwaltung gibt eine Liste von Einheiten mit ihren zugehörigen Mietern zurück. Im lokalen Entwicklungsstand mit 20 Testdatensätzen: 280 Millisekunden. In der Produktionsumgebung mit 1.200 echten Einheiten: 11,8 Sekunden. Faktor 42.
Was N+1 bedeutet – und warum es im Staging unsichtbar bleibt
Der Begriff beschreibt ein einfaches Muster: Eine Query holt N Objekte aus der Datenbank. Für jedes dieser Objekte wird anschließend eine weitere Query ausgeführt, um verknüpfte Daten zu laden. Ergebnis: 1 Query für die Liste, N Queries für die Details, N+1 Queries insgesamt.
Bei 20 Datensätzen macht das 21 Queries. Bei 1.200 sind es 1.201. Das Problem skaliert linear mit der Datenmenge – genau das macht es so tückisch. Im Entwicklungsmodus mit überschaubaren Testdaten bleibt es unter der Wahrnehmungsschwelle. Erst in der Produktion, mit echten Datenmengen, wird es zum Engpass.
Das Überraschende an dem beschriebenen Fall war nicht der Effekt selbst – der ist bekannt. Überraschend war, wie lange es gedauert hat, bis er gemessen wurde. Die Anfrage fühlte sich im Staging „irgendwie langsam" an, aber nicht langsam genug, um Alarm zu schlagen. Erst ein Lasttest mit produktionsnahen Daten hat die 12-Sekunden-Grenze sichtbar gemacht.
Warum ORMs das Problem so gut verstecken
Ein Django-Beispiel, das exakt so ausgesehen hat:
einheiten = Einheit.objects.filter(objekt=objekt_id)
for einheit in einheiten:
mieter = einheit.mietvertrag_set.first()
# verarbeite mieter
Dieser Code ist lesbar. Er ist getestet. Er funktioniert. Und er erzeugt bei jeder Iteration eine neue SQL-Query gegen die Mietvertragstabelle. Das ORM macht das transparent, fast einladend – ein Property-Zugriff, der aussieht wie normaler Python-Code, ist in Wirklichkeit ein Datenbankaufruf.
Das ist keine Kritik an ORM-Frameworks. Sie lösen viele Probleme und machen Entwicklung produktiver. Aber diese Art von Abstraktion hat einen Preis: Sie trennt das mentale Modell vom tatsächlichen Datenbankverhalten. Wer nicht aktiv nach Queries schaut, sieht sie nicht.
Wie man N+1-Queries erkennt
Es gibt mehrere Wege, das Problem sichtbar zu machen – je nach Stack und Zeitpunkt im Entwicklungsprozess.
Query-Logging in der Entwicklung aktivieren
Die direkteste Methode: alle SQL-Queries im Terminal ausgeben lassen. In Django geht das über die LOGGING-Konfiguration:
LOGGING = {
'version': 1,
'handlers': {
'console': {'class': 'logging.StreamHandler'},
},
'loggers': {
'django.db.backends': {
'handlers': ['console'],
'level': 'DEBUG',
},
},
}
Der Output ist oft ernüchternd. Dutzende identisch aussehende Queries, die sich nur im WHERE-Parameter unterscheiden – das ist N+1 in Reinform. Wer das einmal gesehen hat, denkt danach anders über ORM-Zugriffe nach.
Profiling-Tools nutzen
Für Django gibt es den Django Debug Toolbar, der im Browser alle Queries einer Anfrage anzeigt: Anzahl, Dauer, SQL-Text. Ein pragmatischer Schwellenwert: mehr als 20 Queries pro Endpoint-Aufruf sind bei einem Listendpoint fast immer ein Signal.
Rails-Entwickler kennen das Gem Bullet, das N+1-Probleme aktiv erkennt und als Warnung ausgibt – auch für das umgekehrte Problem: unnötige Eager Loads. Ähnliche Tools existieren für Laravel (Laravel Debugbar), für Spring und die meisten anderen Frameworks. Das Prinzip ist überall dasselbe.
Abfrageanzahl in Tests prüfen
Eine weniger bekannte Technik: die Anzahl der SQL-Queries als Teil des Testcases verifizieren. Django bietet dafür assertNumQueries:
def test_einheitenliste_queries(self):
with self.assertNumQueries(3):
response = self.client.get('/api/einheiten/')
self.assertEqual(response.status_code, 200)
Das ist kein Test für die Korrektheit der Daten, sondern für das Datenbankverhalten. Schleicht sich später ein N+1 ein, schlägt der Test an – bevor es in Produktion geht. Dieser Ansatz ist vergleichsweise wenig verbreitet, obwohl der Aufwand minimal ist.
Wie man N+1-Queries behebt
Die Lösung ist fast immer strukturell: Verknüpfte Daten werden nicht einzeln nachgeladen, sondern vorab in einer kontrollierten, zusätzlichen Query mitgezogen.
select_related und prefetch_related in Django
select_related funktioniert über SQL-JOINs und eignet sich für ForeignKey- und OneToOne-Beziehungen:
einheiten = Einheit.objects.filter(objekt=objekt_id).select_related('objekt__adresse')
prefetch_related führt eine separate Query aus und verbindet die Ergebnisse in Python – geeignet für ManyToMany und umgekehrte ForeignKey-Beziehungen:
einheiten = Einheit.objects.filter(objekt=objekt_id).prefetch_related('mietvertrag_set')
Im beschriebenen Fall: 1.201 Queries wurden durch prefetch_related auf 2 reduziert. Die Antwortzeit fiel von 11,8 auf 0,4 Sekunden. Faktor 30 schneller – durch eine Änderung von wenigen Zeichen im Queryset.
Eager Loading in anderen Frameworks
Das Prinzip ist framework-übergreifend identisch:
- Rails:
includes(:mietvertraege) - Laravel:
with('mietvertraege') - TypeORM (Node.js):
relations: ['mietvertraege'] - Hibernate/JPA:
@OneToMany(fetch = FetchType.EAGER)oderJOIN FETCHin JPQL
Die Benennung variiert, das Muster nicht. In jedem Framework gibt es eine Möglichkeit, verknüpfte Daten vorab zu laden – man muss sie nur explizit einsetzen.
Wann ein expliziter JOIN die bessere Antwort ist
Manchmal ist der JOIN die richtigere Lösung, insbesondere wenn Filterbedingungen auf den verknüpften Daten basieren. Eine Datenbankquery, die alle nötigen Spalten per LEFT JOIN auf einmal holt, kann mehrere separate Queries in Gesamtlaufzeit schlagen. Der Preis: der SQL wird expliziter, weniger vom ORM abstrahiert. Das ist manchmal der richtige Kompromiss – vor allem bei komplexen Reporting-Abfragen, bei denen man ohnehin nah an der Datenbankebene arbeitet.
Wann man es nicht sofort fixen sollte
Nicht jedes N+1-Problem verdient sofortige Aufmerksamkeit. Folgende Fragen helfen bei der Einschätzung:
- Wie groß ist die Datenmenge realistisch? Bei Endpoints, die immer unter 50 Objekte zurückgeben, ist der Effekt oft vernachlässigbar.
- Wie häufig wird der Endpoint aufgerufen? Ein Adminbereich mit drei täglichen Zugriffen hat andere Priorität als eine mobile API mit 10.000 Requests pro Stunde.
- Ist Caching die bessere Zwischenmaßnahme? Ein Response-Cache kann das Problem dämpfen, ohne den Code anfassen zu müssen. Keine dauerhafte Lösung, aber eine valide Brücke, wenn die Behebung gerade nicht in den Sprint passt.
- Ist die Abfrage überhaupt gemessen? Optimierung ohne Messung ist Spekulation. Wenn kein Monitoring einen Ausreißer zeigt und kein Nutzer die Latenz gemeldet hat, ist die Energie anderswo sinnvoller eingesetzt.
Was dauerhaft gegen N+1 hilft
Das Entscheidende ist nicht das einmalige Beheben, sondern das strukturelle Verhindern. Drei Praktiken, die sich bewährt haben:
- Query-Count-Tests für kritische Endpoints. Nicht für alles – aber für die Endpoints mit den größten Datenmengen oder meisten Zugriffen lohnt sich ein
assertNumQueries-Test. Er kostet wenig und fängt Regressionen früh ab, bevor sie in Produktion gehen. - Query-Logging standardmäßig im Dev-Profil. Wer während der Entwicklung sieht, wie viele Queries eine Aktion erzeugt, denkt anders über Datenstrukturen nach. Es braucht keine Policy – nur sichtbare Information zum richtigen Zeitpunkt.
- Datenbankblick in Code-Reviews. Die Frage „Welche Queries erzeugt diese Änderung?" ist kein Overhead, sondern ein Qualitätsstandard. Sie lässt sich als Checkbox in Pull-Request-Templates einbauen – und verändert, wie Entwickler Code schreiben, bevor er reviewed wird.
Wer Systeme für wachsende Datenmengen baut – ob Immobilienverwaltung, Auftragsverfolgung oder Kundenportale –, wird dem N+1-Problem früher oder später begegnen. Die Frage ist, ob man es in der Staging-Umgebung mit 20 Datensätzen findet oder in der Produktion mit 20.000, wenn Nutzer warten. Weitere Einblicke in die Entwicklungspraxis solcher Systeme finden sich auf innosirius.de.
Die gute Nachricht: Das Werkzeug zum Erkennen und Beheben existiert in jedem relevanten Framework. Es muss nur benutzt werden.
Weitere Beiträge

Datensilos im B2B aufbrechen: So gelingen Integrationen
Datensilos bremsen Wachstum und kosten täglich Zeit. Erfahren Sie, wie B2B-Unternehmen Systeme smart verbinden und echten Mehrwert aus ihren Daten ziehen. Jetzt lesen.

GEO: So werden B2B-Unternehmen in KI-Suchen sichtbar
Generative Engine Optimization verändert die digitale Sichtbarkeit. Erfahren Sie, wie B2B-Unternehmen ihre KI-Reichweite gezielt ausbauen. Jetzt lesen.

Datensilos vermeiden: Softwareintegration für Immobilienprofis
Warum fragmentierte Systeme Immobilienunternehmen bremsen und wie durchdachte Softwareintegration operative Effizienz schafft. Praktische Strategien für B2B-Entscheider.