Wie lässt sich das N+1-Problem in GraphQL-APIs mittels DataLoader oder ähnlichen Batching-Strategien effizient lösen?

Das N+1-Problem in GraphQL entsteht durch die isolierte Ausführung von Resolver-Funktionen. Wenn eine Abfrage eine Liste von Objekten und deren verknüpfte Ressourcen anfordert, führt der Root-Resolver eine Abfrage aus (1), gefolgt von einer Einzelabfrage für jede Ressource der Liste (N).

Wir lösen dieses Problem durch den Einsatz von DataLoader, einem Utility, das zwei Mechanismen kombiniert: Batching und Caching.

Funktionsweise von DataLoader

DataLoader arbeitet asynchron und nutzt den Event-Loop von Node.js. Anstatt sofort eine Datenbankabfrage zu starten, sammelt der Loader alle angeforderten Schlüssel innerhalb eines Ticks. Sobald der Tick endet, wird eine einzige Batch-Funktion aufgerufen, die alle gesammelten IDs in einer einzigen Abfrage (z. B. via WHERE IN (...)) verarbeitet.

MerkmalStandard-ResolverDataLoader-Strategie
Datenbank-CallsN + 1 Abfragen2 Abfragen (1 Liste, 1 Batch)
AusführungsmodusSequenziell / UnabhängigGruppiert pro Event-Loop-Tick
RedundanzMehrfache Abfragen identischer IDsCaching innerhalb des Request-Scopes
LatenzHoch durch Netzwerk-RoundtripsNiedrig durch minimierte Roundtrips

Implementierungsschritte

  1. Batch-Funktion definieren: Wir erstellen eine Funktion, die ein Array von Schlüsseln entgegennimmt und ein Array von Ergebnissen in der exakt gleichen Reihenfolge zurückgibt.
  2. Loader-Instanziierung: Der DataLoader wird pro Request instanziiert, um Datenlecks zwischen verschiedenen Benutzern zu vermeiden.
  3. Resolver-Integration: Im Resolver rufen wir nicht mehr direkt die Datenbank auf, sondern nutzen die .load()-Methode des Loaders.

Für komplexe Datenstrukturen integrieren wir diese Logik in unsere Strategien für Data Engineering, um die Performance auf Datenbankebene zu optimieren. Alternativ nutzen wir bei extrem hohen Lasten "Look-ahead"-Strategien, bei denen wir den GraphQL-AST (Abstract Syntax Tree) analysieren, um Joins bereits im ersten Resolver-Aufruf zu generieren.

Wir empfehlen den Einsatz von DataLoader als Standard für die meisten Anwendungsfälle, da er die Komplexität der Resolver gering hält. In Szenarien mit extrem tiefen Verschachtelungen und massiven Datenmengen ist jedoch ein Wechsel zu AST-basierten Joins vorzuziehen, da selbst Batching bei sehr vielen Ebenen die Anzahl der Roundtrips nicht auf ein absolutes Minimum reduziert.

Sergej Wiens

Sergej Wiens

Gründer & Software Architekt