Warum wird das N+1-Problem in Django ORM so häufig erwähnt?

Wenn man Django ORM nutzt, hört man oft den Begriff "N+1-Problem". Ohne es selbst erfahren zu haben, ist es jedoch schwierig, die Schwere dieses Problems zu erkennen.

Einfach gesagt ist es „das Problem, dass eine Query, die nur einmal ausgeführt werden sollte, viel öfter ausgeführt wird als erwartet“. Infolgedessen kann die Ladegeschwindigkeit der Seite erheblich verlangsamt werden, und je mehr Daten es gibt, desto schwerwiegender wird der Leistungsabfall. Besonders tritt es häufig auf, wenn daten mit Beziehungen zwischen Modellen abgefragt werden, weshalb man es beim Einsatz von ORM zwingend verstehen sollte.

💡 Lass uns das N+1-Problem einfach verstehen

Angenommen, wir erstellen ein Blog-System. Jeder Autor kann mehrere Beiträge schreiben. Wenn wir dies in Django-Modellen darstellen, sieht es so aus:

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Post(models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField()
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

Da jeder Autor mehrere Beiträge schreiben kann, handelt es sich um eine 1:N-Beziehung.

Nun nehmen wir an, dass wir eine Funktion implementieren, die die Namen aller Autoren und die Titel der von ihnen verfassten Beiträge ausgibt. In der Regel wird man dies wahrscheinlich so schreiben:

⚠️ Code, bei dem das N+1-Problem auftritt

authors = Author.objects.all()  # (1) Erste Query (Abfrage der Author-Tabelle)

for author in authors:
    print(author.post_set.all())  # (N) Einzelne Abfragen für die Posts jedes Autors

Wenn man es einmal ausführt, kann es länger dauern, als man erwartet. Wenn die Daten nicht zahlreich sind, wird es vielleicht nicht auffallen, aber was, wenn es 100 Autoren und mehr als 500 Beiträge gibt? Dann wird das Problem langsam sichtbar.

🧐 Tatsächlich ausgeführte SQL-Abfragen

SELECT * FROM author;  -- 1. Ausführung
SELECT * FROM post WHERE author_id = 1;  -- N. Ausführung
SELECT * FROM post WHERE author_id = 2;
SELECT * FROM post WHERE author_id = 3;
...

Je mehr Autoren es gibt, desto exponentiell steigt die Anzahl der Abfragen. Das ist das N+1-Problem.

🚀 In welchen Situationen tritt das N+1-Problem häufig auf?

Dieses Problem muss man praktisch immer im Hinterkopf haben, wenn zwei oder mehr Modelle zusammenhängende Daten abfragen, ganz gleich, ob man Django ORM gut oder schlecht benutzt. Besonders tritt es häufig in den folgenden Fällen auf:

1️⃣ Zugriff auf Daten innerhalb eines Templates in einer Schleife

{% for author in authors %}
    

{{ author.name }}

    {% for post in author.post_set.all %}
  • {{ post.title }}
  • {% endfor %}
{% endfor %}

Wenn man in einem Template eine Schleife hat, die auf Daten zugreift, entstehen zusätzlich für jedes Objekt Queries, was die Leistung verschlechtert.

2️⃣ Zugriff auf verwandte Modelle innerhalb einer Schleife in einer View-Funktion

def blog_view(request):
    authors = Author.objects.all()  # Erste Query (Abfrage der Authors)
    
    author_list = []
    for author in authors:
        author_list.append({
            'name': author.name,
            'posts': author.post_set.all()  # N zusätzliche Queries entstehen
        })
    
    return render(request, 'blog.html', {'authors': author_list})

Selbst wenn man die Daten in einer View-Funktion verarbeitet, kann dasselbe Problem auftreten.

💡 Meine Erfahrung mit dem N+1-Problem

Früher habe ich einmal eine benutzerdefinierte Methode namens get_absolute_url() erstellt. Es handelte sich um eine Funktion, die URLs aus mehreren Feldern des Modells generierte, und erst nachdem ich Leistungsprobleme hatte, fand ich die Ursache.

class Post(models.Model):
    title = models.CharField(max_length=255)
    slug = models.SlugField(unique=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

    def get_absolute_url(self):
        return f"/author/{self.author.name}/{self.slug}/"

Bei jedem Aufruf dieser Methode wird self.author.name ausgeführt, und N zusätzliche Queries entstehen. Es hat eine Weile gedauert, bis ich dieses Problem gelöst habe.

✅ Zusammenfassung: Warum man das N+1-Problem verstehen muss

  • Solange man Django ORM verwendet, kann dieses Problem jederzeit auftreten.
  • Wenn die Seitenladegeschwindigkeit langsam ist, besteht eine hohe Wahrscheinlichkeit, dass ORM mehr Queries als erwartet ausführt.
  • Es kann in Templates, View-Funktionen oder Modellmethoden auftreten.
  • Je mehr Daten vorhanden sind, desto schwerwiegender kann die Leistungsverschlechterung sein.
  • Da ORM keine automatischen Optimierungen vornimmt, muss man die ausgeführten Queries selbst überprüfen und optimieren.

Im nächsten Beitrag werden wir besprechen, wie man das N+1-Problem mit select_related und prefetch_related löst.

N+1 Query Problem in Django ORM