Start / Blog

Blog

N+1 in async SQLAlchemy: aus einem Performance-Problem wird ein Fehler

Das N+1-Problem kennt fast jeder: Eine Abfrage holt die Eltern, und dann kommt für jedes Kind noch eine dazu. Bei drei Datensätzen sind das vier Abfragen, bei tausend eintausendundeine. Der Code sieht dabei völlig normal aus.

Mit Testdaten sieht es harmlos aus

Genau darin liegt die Tücke. Der Unterschied zwischen vier und zwei Abfragen fällt bei drei Testdatensätzen niemandem auf — weder in der Code-Review noch im Testlauf. Ein Zähler am Ereignis before_cursor_execute macht daraus eine Zahl. Interessant ist dabei nicht die Zahl selbst, sondern ob sie mitwächst.

Async verzeiht nicht — zum Glück

Jetzt kommt der Teil, der überrascht. In synchronem SQLAlchemy feuert eine nicht geladene Beziehung beim Zugriff still eine Nachladeabfrage ab. Der Code funktioniert, nur eben langsam. In async gibt es dafür keinen sicheren Weg, also wirft SQLAlchemy einen MissingGreenlet-Fehler.

Beim ersten Mal fühlt sich das wie Schikane an. Tatsächlich ist es ein Frühwarnsystem: Aus einem Performance-Problem, das man Monate später in der Statistik der Antwortzeiten sucht, wird ein Programmierfehler beim Entwickeln.

Laden ist eine Zusage, keine Optimierung

Damit verschiebt sich, wofür man selectinload und joinedload überhaupt benutzt. Es ist keine nachträgliche Optimierung mehr, sondern eine Entscheidung beim Schreiben der Abfrage:

  • Sammlungen über selectinload — zwei Abfragen, unabhängig von der Menge
  • Einzelbezüge über joinedload — ein Join, ohne Zeilen zu vervielfachen
  • Verschachtelte Pfade brauchen eine Option je Sprung

Die Leitfrage lautet: Welche verbundenen Daten sind garantiert geladen, wenn diese Abfrage zurückkommt?

Mehr dazu im Seminar

Wie sich das messen, beheben und als Regel verankern lässt, zeigt das Modul Ladestrategien und Produktionsreife — mit Video und Kapitelübersicht. Zum Nachschlagen gibt es das FastAPI-Cheat-Sheet.

Zum Seminar FastAPI Architektur in der Praxis

← Alle Beiträge