Start / Seminare / FastAPI Architektur in der Praxis

Modul

Ladestrategien und Produktionsreife

4 Kapitel in diesem Modul-Video · Laufzeit

Vertonung mit synthetischer Stimme · Inhalte redaktionell verantwortet · © 2026 HECKER CONSULTING, alle Rechte vorbehalten

Transkript

Der gesprochene Text dieses Moduls zum Mitlesen, Überfliegen und Durchsuchen. Ein Klick auf einen Zeitstempel springt an die Stelle im Video.

Ladestrategien und Produktionsreife

0:00 Es gibt eine Sorte Fehler, die in der Entwicklung nie auffällt und in Produktion sofort. Sie hat einen Namen — das N-plus-eins-Problem — und sie ist wahrscheinlich der häufigste Grund dafür, dass eine Anwendung mit echten Daten plötzlich zäh wird. Das Tückische daran: Mit drei Testdatensätzen sieht alles gut aus. Der Unterschied zwischen vier und zwei Abfragen fällt niemandem auf.

0:21 Mit tausend Datensätzen sind es tausendundeine. In diesem Modul machen wir diesen Fehler zuerst sichtbar, beheben ihn dann, und arbeiten am Ende die Frage ab, was zwischen unserer Übung und einem produktionsreifen Zugriff liegt.

Der N+1-Endpunkt

0:35 Fangen wir mit dem Messen an, nicht mit dem Beheben. Das ist keine Pedanterie: Solange Sie nicht zählen, wie oft Ihre Anwendung die Datenbank anspricht, bleibt jede Aussage über Performance eine Vermutung. Wir bauen uns deshalb zuerst einen kleinen Zähler und führen dann den Klassiker vor. Sie werden sehen, dass er in diesem Beispiel geradezu harmlos aussieht — und genau das ist der Punkt.

0:57 Der Name beschreibt das Problem ziemlich genau. Eine Abfrage holt die Eltern — in unserem Fall alle Angebote. Und dann kommt für jedes einzelne Angebot noch eine Abfrage dazu, um dessen Positionen nachzuladen. Bei drei Angeboten sind das vier Abfragen. Denken Sie an jemanden, der im Supermarkt für jeden einzelnen Artikel auf seiner Liste noch einmal von vorne durch den Laden läuft.

1:20 Das Ergebnis stimmt am Ende, aber der Weg ist absurd. Und im Gegensatz zum Supermarkt merkt man es dem Code nicht an: Er sieht völlig normal aus. Der rote Faden hier ist: erst sichtbar machen, dann urteilen. SQLAlchemy verschickt vor jeder Anweisung ein Ereignis, und daran hängen wir schlicht einen Zähler. Verpackt in einen kleinen Kontextmanager lässt sich damit jeder Codeblock vermessen.

1:44 Der letzte Schritt ist der wichtigste und wird oft weggelassen: Rufen Sie denselben Endpunkt mit drei und mit hundert Datensätzen auf. Erst dieser Vergleich zeigt das Problem. Eine einzelne Zahl sagt gar nichts — entscheidend ist, ob sie mitwächst. Lesen Sie diese Tabelle spaltenweise, dann sehen Sie den ganzen Punkt. Die mittlere Spalte wächst mit den Daten, die rechte nicht.

2:07 Das ist der Unterschied zwischen einem Dienst, der mit dem Geschäft mitwächst, und einem, der irgendwann bei einem bestimmten Datenbestand umkippt. Und beachten Sie die oberste Zeile: Vier gegen zwei. In einer Entwicklungsumgebung mit drei Testdatensätzen ist dieser Unterschied schlicht nicht wahrnehmbar. Genau deshalb überlebt dieser Fehler so zuverlässig jede Code-Review und jeden Testlauf — und meldet sich erst, wenn der erste größere Kunde da ist.

2:33 Die ersten beiden Punkte gehören zusammen und sind eine gute Nachricht. In klassischem, synchronem Code hätte SQLAlchemy die fehlenden Daten still nachgeladen — Ihr Code hätte funktioniert, nur langsam. Im asynchronen Betrieb gibt es dafür keinen sicheren Weg, also wirft SQLAlchemy stattdessen eine Ausnahme. Das fühlt sich beim ersten Mal wie eine Schikane an.

2:54 Tatsächlich ist es ein Frühwarnsystem: Der Fehler zeigt sich beim Entwickeln statt in der Statistik der Antwortzeiten drei Monate später. Aus einem Performance-Problem wird ein Programmierfehler, und die sind deutlich leichter zu finden.

N+1 mit Eager Loading beheben

3:08 Das Problem ist sichtbar, jetzt beheben wir es. Die Lösung besteht im Kern aus einer einzigen zusätzlichen Angabe an der Abfrage — technisch also eine Kleinigkeit. Interessant ist die Entscheidung dahinter: Es gibt zwei Varianten, und sie sind grundverschieden. Diese Entscheidung ist der eigentliche Inhalt des Kapitels, denn die falsche Wahl macht die Sache in bestimmten Fällen nicht besser, sondern schlimmer.

3:31 Und sie fällt einem dabei nicht auf, weil das Ergebnis ja stimmt. Eager Loading heißt nichts anderes, als SQLAlchemy im Voraus zu sagen, welche verbundenen Daten mitkommen sollen. Es gibt dafür zwei Wege, und sie sind grundverschieden. Der eine setzt eine zweite Abfrage ab, die alle Kinder auf einmal holt — bei jeder Datenmenge genau zwei Abfragen insgesamt.

3:53 Der andere holt alles in einer einzigen Abfrage über einen Join. Eins klingt besser als zwei, deshalb greifen viele reflexhaft zum Join. Auf der übernächsten Folie sehen Sie, warum das bei Listen ein teurer Reflex ist. Zwei fast identische Funktionen, und der ganze Unterschied steckt in einem Wort. Die eine lädt eine Liste — die Positionen eines Angebots.

4:16 Die andere lädt einen Einzelbezug — den einen Veranstalter. Worauf es ankommt, ist nicht die Syntax, sondern der Ort: Die Entscheidung steht in der Abfrage, nicht am Modell. Dort, wo auch entschieden wird, was der Aufrufer eigentlich braucht, und dort, wo die Kosten anfallen. Wer diese Angabe ans Modell hängt, nimmt allen Abfragen die Entscheidung ab — auch denen, die die Daten gar nicht brauchen.

4:40 Rechnen wir das Beispiel einmal durch, dann wird der Punkt greifbar. Drei Angebote mit je vier Positionen ergeben über einen Join zwölf Zeilen. Und in jeder dieser zwölf Zeilen stehen die Angebotsdaten noch einmal komplett drin — Titel, Zeitraum, Rabatt, alles. Die Datenbank schickt also dieselben Daten viermal über die Leitung, nur um jeweils eine andere Position mitzuliefern.

5:03 Bei einem Einzelbezug passiert nichts dergleichen: Ein Angebot hat genau einen Veranstalter, der Join fügt genau eine Zeile Daten hinzu. Deshalb die Regel: Listen über die zweite Abfrage, Einzelbezüge über den Join. Der entscheidende Teil dieser Aufgabe ist die Gegenprobe. Es reicht nicht, dass der Zähler nach Ihrer Änderung auf zwei steht — er stand ja vorher auch schon bei einer kleinen Zahl.

5:27 Verzehnfachen Sie den Datenbestand und schauen Sie noch einmal hin. Bleibt die Zahl bei zwei, haben Sie es richtig gemacht. Wächst sie mit, laden Sie irgendwo noch nach, ohne es zu merken. Diese Gegenprobe ist übrigens ein gutes Muster für den Alltag: Eine Kennzahl, die sich bei wachsenden Daten nicht verändert, ist eine Zusage.

5:46 Eine, die sich verändert, ist ein Zufall.

Die Ladestrategie bewusst wählen

5:49 Wir haben einen konkreten Fehler behoben. Jetzt machen wir daraus ein Entscheidungsraster, das für jede Abfrage trägt, die Sie künftig schreiben. Die Leitfrage lautet dabei immer gleich, und sie ist erstaunlich einfach: Welche verbundenen Daten sind garantiert geladen, wenn diese Abfrage zurückkommt? Wer sie beim Schreiben beantwortet, bekommt später keine Überraschungen.

6:10 Wer sie offenlässt, überlässt die Antwort dem Zufall — und der fällt in der Entwicklung anders aus als im Betrieb. Der wichtigste Satz dieses Kapitels steht in der Definition: Laden ist in asynchronem Code keine Optimierung mehr, sondern eine Zusage. In der synchronen Welt war das anders. Dort konnte man das Thema ignorieren, und die Sache funktionierte trotzdem — eben nur langsam. Optimieren konnte man später, wenn es weh tat.

6:36 Im asynchronen Betrieb geht das nicht mehr: Was Sie nicht ausdrücklich laden, können Sie nicht berühren. Das zwingt zu einer Entscheidung beim Schreiben der Abfrage. Und ehrlich gesagt: Diese Entscheidung hätte man auch vorher schon treffen sollen. Die Tabelle ist Ihre Landkarte, aber die interessanteste Zeile ist die unterste. Die anderen laden etwas, diese wirft einen Fehler.

6:59 Was zunächst unsinnig klingt, ist ein sehr wirksames Werkzeug: Sie verbieten damit ausdrücklich jedes ungeplante Nachladen. Eine Abfrage, die vergessen hat, ihre Zusage zu machen, scheitert dann sofort und laut — in der Entwicklung, nicht im Betrieb. Aus einem Fehler, den man zufällig entdeckt, wird eine Regel, die sich selbst durchsetzt. Und die vorletzte Zeile brauchen Sie nur, wenn Sie älteren Code lesen; sie ist abgelöst.

7:25 Es gibt eine Möglichkeit, die Ladestrategie am Modell festzulegen. Sie sieht bequem aus: einmal gesetzt, gilt sie überall, und niemand kann sie mehr vergessen. Der Preis ist Sichtbarkeit. Wer die Repository-Funktion liest, sieht nicht mehr, was sie tatsächlich lädt — das steht ja woanders. Und jeder Aufrufer bezahlt das Laden mit, auch wenn er die Liste gar nicht anschaut.

7:47 Es ist derselbe Handel wie bei vielen bequemen Voreinstellungen: Sie nehmen Ihnen eine Entscheidung ab und verstecken dabei die Kosten. Bei Datenbanken sind diese Kosten selten klein. Der dritte Punkt trifft alle, die zum ersten Mal über zwei Ebenen laden wollen. Eine Angabe lädt genau einen Sprung — vom Angebot zu den Positionen.

8:07 Wollen Sie von dort noch eine Ebene tiefer, brauchen Sie eine zweite Angabe für diesen zweiten Sprung. Das ist keine Schikane, sondern konsequent: Jeder Sprung ist eine eigene Zusage und hat eigene Kosten. Wer das nicht weiß, bekommt beim ersten verschachtelten Zugriff wieder die Ausnahme aus dem ersten Kapitel und wundert sich, weil er das Laden doch angegeben hat — nur eben nicht für diese Ebene.

Was vor Produktion zu ändern ist

8:31 Unsere Speicherschicht funktioniert jetzt von Ende zu Ende. Damit stellt sich die ehrliche Frage: Was davon würden wir ändern, bevor das Ganze echte Kunden bedient? Drei Dinge haben wir bewusst vereinfacht, um uns auf die eigentlichen Themen konzentrieren zu können. Jede dieser Vereinfachungen hat eine klare, produktionstaugliche Antwort — und die schauen wir uns jetzt an.

8:52 Die drei Vereinfachungen betreffen drei verschiedene Ebenen, und das ist kein Zufall. Die erste ist die Frage, wie das Schema überhaupt entsteht. Die zweite ist die Wahl der Datenbank. Die dritte ist die Menge an Daten, die eine einzelne Abfrage bewegt. Man kann eine Anwendung sehr lange betreiben, ohne über die dritte nachzudenken — bis zu dem Tag, an dem eine Tabelle groß genug geworden ist.

9:15 Die ersten beiden dagegen sollte man klären, bevor überhaupt etwas produktiv geht, denn nachträglich sind sie deutlich unangenehmer. Bemerkenswert an dieser Tabelle ist die mittlere Zeile, genauer gesagt die Fußnote darunter. Der Wechsel von der Übungsdatenbank zu einer Serverdatenbank ist eine einzige Zeile — die Verbindungsadresse.

9:35 Engine, Session-Fabrik, Modelle, Repository-Funktionen: alles bleibt, wie es ist. Das ist der eigentliche Gewinn davon, gegen die Abstraktion von SQLAlchemy zu arbeiten statt gegen eine bestimmte Datenbank. Man merkt es erst zu schätzen, wenn man einmal das Gegenteil erlebt hat — eine Anwendung, in der die Eigenheiten eines Herstellers über hundert Dateien verteilt sind.

9:58 Schauen Sie sich einmal genau an, was unsere Startfunktion tut: Sie verwirft jede Tabelle und legt sie neu an. Für eine Übung ist das ideal — jeder Lauf beginnt sauber. In Produktion wäre derselbe Code eine Katastrophe bei jedem Deployment. Aber selbst wenn man das Verwerfen weglässt, bleibt eine zweite Einschränkung: Das automatische Anlegen erzeugt nur fehlende Tabellen. Es ändert nie eine bestehende.

10:22 Fügen Sie also eine Spalte zu einem Modell hinzu, passiert schlicht nichts, und Ihre Anwendung scheitert an einer Spalte, die es in der Datenbank nicht gibt. Der rote Faden dieser fünf Punkte ist ein Haushalten mit einer knappen Ressource. Verbindungen zu einer Serverdatenbank sind teuer — Netzwerkweg, Anmeldung, Aufbau.

10:41 Deshalb hält die Engine einen Vorrat offen, aus dem sich die Sessions bedienen. Die ersten drei Schritte legen fest, wie groß dieser Vorrat ist und wie lange jemand wartet, wenn nichts frei ist. Der letzte Schritt ist der, der im Alltag am meisten bringt und nichts kostet: Sessions spät öffnen und früh schließen. Denn im asynchronen Betrieb sind hunderte Anfragen gleichzeitig unterwegs, und jede hält ihre Verbindung fest, solange ihre Session lebt.

11:07 Die ersten beiden Punkte betreffen ein Thema, das man erst spät ernst nimmt: Blättern in großen Ergebnismengen. Die einfache Variante — überspringe die ersten hundert Zeilen, gib mir die nächsten zwanzig — funktioniert wunderbar, solange die Seitenzahl klein bleibt. Auf Seite fünfhundert muss die Datenbank aber tatsächlich zehntausend Zeilen ablaufen, nur um sie wegzuwerfen.

11:29 Die stabilere Variante merkt sich stattdessen, wo man aufgehört hat, und fragt nach dem, was danach kommt. Mit einem Index ist dieser Sprung sofort erledigt, unabhängig davon, wie tief man schon ist. Der Kern dieser Aufgabe ist ein Perspektivwechsel. Bisher hat der Endpunkt zurückgegeben, was zufällig in der Tabelle stand.

11:48 Künftig entscheidet die Abfrage, wie viel sie bewegt — unabhängig vom Bestand. Das Erfolgskriterium ist entsprechend nicht, dass eine Seite herauskommt, sondern dass die Antwortzeit gleich bleibt, wenn die Daten wachsen. Legen Sie also ruhig ein paar tausend Angebote an und messen Sie zweimal. Diese Art von Zusage — die Antwortzeit hängt nicht vom Datenbestand ab — ist am Ende das, was einen Dienst betreibbar macht.

Was jetzt trägt

12:13 Fassen wir zusammen. Abfragen sind jetzt zählbar, damit ist das N-plus-eins-Problem eine Zahl und keine Vermutung mehr. Die Ladestrategie steht an der Abfrage und richtet sich nach der Art der Beziehung. Das Repository sagt zu, was geladen ist, und die Aufrufer lösen keine Nachladeabfragen mehr aus. Und für Schema, Datenbank und Datenmenge haben wir je eine produktionstaugliche Antwort. Damit ist die Speicherschicht fertig.

12:38 Im nächsten Modul geht es um etwas anderes: darum, wie all diese Teile eigentlich zusammenfinden, ohne dass jeder Endpunkt sie selbst zusammenbaut.

Lieber mit Trainer? Dieses Modul ist Teil unserer Team-Schulungen — mit Übungen, Ihrem eigenen Code und Fragen, die ein Video nicht beantwortet. Mehr erfahren →