Jede gut funktionierende API stößt irgendwann auf ein Problem, das zunächst unsichtbar bleibt: das N+1-Abfrageproblem. Es verursacht keine Abstürze, keine Fehler – und doch macht es Ihre Endpunkte mit wachsender Datenmenge immer langsamer. Erst wenn Sentry oder ähnliche Tools es in der Produktion melden, wird es sichtbar. Genau das passierte mir kürzlich am Endpunkt /api/blog-posts/ meiner Django-REST-API. Hier ist die genaue Ursache und wie ich das Problem mit minimalem Aufwand gelöst habe.
Was ist eine N+1-Abfrage – und warum ist sie gefährlich?
Eine N+1-Abfrage entsteht, wenn Ihre Anwendung zunächst eine Liste mit N Datensätzen abruft und anschließend für jeden einzelnen Datensatz eine zusätzliche Abfrage stellt, um zugehörige Daten zu laden. Statt zwei oder drei effizienten Datenbankabfragen summieren sich diese auf 1 + N Abfragen.
In Django bleibt dieses Problem oft unbemerkt, weil das ORM standardmäßig lazy arbeitet. Wird auf ein nicht vorgeladenes Beziehungsfeld eines Modells zugegriffen, führt Django sofort eine neue SELECT-Abfrage aus. Bei 30 Blogbeiträgen bedeutet das 30 zusätzliche, unnötige Datenbankzugriffe – obwohl im Code nur zwei Beziehungen in der Serializer-Klasse definiert sind.
Der fehlerhafte Code und seine Ursache
Die BlogPostViewSet-Klasse sah auf den ersten Blick sauber aus:
class BlogPostViewSet(viewsets.ReadOnlyModelViewSet):
queryset = BlogPost.objects.all()
serializer_class = BlogPostSerializer
lookup_field = "uid"Der zugehörige Serializer definierte zwei Beziehungen:
class BlogPostSerializer(serializers.ModelSerializer):
tags = BlogTagSerializer(many=True, read_only=True)
series = BlogSeriesSerializer(read_only=True)Das Problem: BlogPost verfügte über zwei Beziehungen:
series– ein Fremdschlüssel aufBlogSeriestags– eine ManyToMany-Beziehung zuBlogTag
Wenn DRF eine Liste von 30 Blogbeiträgen serialisiert, greift es auf post.series und post.tags für jeden Beitrag zu. Ohne vorgeladene Daten führt Django pro Beitrag zwei zusätzliche Abfragen aus – eine für die Serie und eine für die Tags. Bei 30 Beiträgen summiert sich das auf 1 + 60 Abfragen – nur für eine einzelne API-Anfrage.
Das gleiche Problem trat in der featured-Aktion auf:
@action(detail=False, methods=["get"])
def featured(self, request):
queryset = BlogPost.objects.filter(
date_published__isnull=False
).order_by("-date_published")[:3]Auch hier fehlte die vorgeladene Abfrage, obwohl nur die drei neuesten Beiträge abgefragt wurden.
Die Lösung: select_related und prefetch_related
Django bietet zwei mächtige Methoden, um N+1-Abfragen zu vermeiden:
- `select_related()` – Optimiert Fremdschlüssel (
ForeignKey) und OneToOne-Beziehungen durch einen SQL-Join. Alles wird in einer einzigen Abfrage geladen. - `prefetch_related()` – Lädt ManyToMany-Beziehungen und Reverse-ForeignKeys in einer separaten Abfrage und speichert die Ergebnisse im Python-Speicher.
Die Korrektur erforderte nur eine kleine Anpassung:
class BlogPostViewSet(viewsets.ReadOnlyModelViewSet):
queryset = BlogPost.objects.select_related("series").prefetch_related("tags")
serializer_class = BlogPostSerializer
lookup_field = "uid"
@action(detail=False, methods=["get"])
def featured(self, request):
queryset = (
BlogPost.objects
.select_related("series")
.prefetch_related("tags")
.filter(date_published__isnull=False)
.order_by("-date_published")[:3]
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)Das Ergebnis: Unabhängig von der Datenmenge führt der Endpunkt nun nur noch drei Abfragen aus:
- Abfrage aller Blogbeiträge
- Abfrage aller zugehörigen Serien
- Abfrage aller zugehörigen Tags
Ein zusätzlicher Fund: Das gleiche Problem im TestimonialViewSet
Während der Überprüfung des Blog-Endpunkts entdeckte ich das gleiche Muster im TestimonialViewSet. Der Serializer griff auf project.title und project.slug zu, doch die zugrundeliegende Abfrage war nicht optimiert:
# Vorher
queryset = Testimonial.objects.all()
# Nachher
queryset = Testimonial.objects.select_related("project")Eine einzige Zeile Code – und schon war das N+1-Problem auch hier behoben.
So erkennen Sie N+1-Abfragen in Ihrem eigenen Code
Das Muster ist immer gleich: Achten Sie in ViewSets oder Views auf folgende Hinweise:
- Die Abfrage verwendet weder
select_related()nochprefetch_related(). - Der Serializer greift auf Beziehungsfelder zu, z. B. über
source="relation.field", verschachtelte Serializer oderSerializerMethodField, das aufobj.relationzugreift.
Tools, die Ihnen helfen, das Problem frühzeitig zu erkennen:
- django-debug-toolbar – Zeigt die Anzahl der Abfragen pro Request im Browser an.
- nplusone – Wirft Ausnahmen in Tests auf, sobald N+1-Abfragen erkannt werden.
- Sentry Performance – Erkennt das Problem in der Produktion durch detaillierte Abfrage-Traces.
Der beste Zeitpunkt, um N+1-Abfragen zu finden, ist während der Code-Review. Jedes Mal, wenn Sie einen verschachtelten Serializer einführen, sollten Sie sich fragen: Lädt die Abfrage für diesen View die zugehörige Beziehung vor?
Fazit: Ein einfacher Grundsatz für mehr Performance
Die Lazy-Evaluation des Django-ORM ist ein Feature – kein Fehler. Doch sie erfordert Disziplin bei der Abfrageplanung. Ein scheinbar sauberes ViewSet mit objects.all() verbirgt oft einen Sturm an unnötigen Abfragen direkt hinter dem Serializer.
Merken Sie sich diesen Grundsatz: Jede Beziehung, die im Serializer genutzt wird, benötigt eine entsprechende `select_related()`- oder `prefetch_related()`-Anweisung im Abfrage-Set. Machen Sie das zu einem festen Bestandteil Ihrer Pull-Requests – und sparen Sie sich so teure Performance-Probleme in der Produktion.
Die nächste Generation der API muss nicht langsamer werden, nur weil Ihre Datenmenge wächst. Mit diesen kleinen Änderungen stellen Sie sicher, dass Ihre Endpunkte auch bei steigender Last performant bleiben.
KI-Zusammenfassung
Django REST Framework kullanarak geliştirdiğiniz API'de gizlenen performans katili N+1 sorgusunu nasıl tespit edip, sadece üç satırlık kod değişikliğiyle nasıl çözebileceğinizi öğrenin.