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.