Performance: N+1 und DataLoader
Feldauflösungen bündeln und cachen, damit alles schnell bleibt
Performance: N+1 und DataLoader ist eine kostenlose PHP Academy-Lektion auf CoddyKit. Dies ist Lektion 4 von 4. Du kannst die komplette Lektion unten kostenlos lesen – dann übst du sie direkt im Browser mit einem integrierten Code-Editor und einem KI-Tutor rund um die Uhr. Sie ist Teil des PHP Academy-Lernpfads, und dein Fortschritt wird über Web und CoddyKit-App synchronisiert. Der PHP Academy-Kurs umfasst insgesamt 4 Lektionen.
Der stille Killer: N+1
GraphQLs größte Performance-Falle ist das N+1-Abfrageproblem. Es bleibt unbemerkt, weil jeder Resolver für sich genommen harmlos aussieht – aber sobald Sie eine Liste und ein Feld pro Element verschachteln, führen Sie eine Abfrage für die Liste plus eine Abfrage pro Element aus. Bei 100 Elementen sind das 101 Roundtrips. In dieser Lektion erfahren Sie, wie Sie daraus mit DataLoader eine Handvoll gebündelter Abfragen machen.
N+1 konkret betrachtet
Betrachten Sie { posts { author { name } } }. Der posts-Resolver führt eine Abfrage aus. Danach führt der author-Resolver für jeden Beitrag seine eigene Abfrage aus. Der naive Code unten macht die Kosten deutlich.
<?php
// 1 query for posts...
$posts = [['id'=>1,'author_id'=>7],['id'=>2,'author_id'=>7],['id'=>3,'author_id'=>9]];
$queries = 1;
foreach ($posts as $post) {
// ...then 1 query PER post to fetch its author
$queries++;
// SELECT * FROM users WHERE id = $post['author_id']
}
echo "Total DB queries: {$queries}\n"; // Total DB queries: 4
Die Erkenntnis: Nach Ebenen bündeln
GraphQL löst Felder Ebene für Ebene auf. Alle author-Resolver für die Beitragsliste laufen im selben Ausführungstakt. Wenn wir jede Autorenabfrage zurückstellen, die angeforderten IDs sammeln und anschließend eine einzelne WHERE id IN (...)-Abfrage ausführen können, machen wir aus N Abfragen eine. Genau diese Zurückstellung ermöglicht GraphQL\Deferred.
<?php
$ids = [7, 7, 9];
$unique = array_values(array_unique($ids));
// One query instead of three:
echo 'SELECT * FROM users WHERE id IN (' . implode(',', $unique) . ")\n";
// SELECT * FROM users WHERE id IN (7,9)
Ein minimaler Buffer/Loader
Hier sehen Sie das Grundprinzip eines DataLoaders: ein Buffer, der IDs sammelt, sie einmal über eine Batch-Funktion lädt und Ergebnisse aus einem Cache bereitstellt. Derselbe Schlüssel, der zweimal angefordert wird, wird nur einmal geladen – automatische Deduplizierung.
<?php
class UserLoader {
private array $queue = [];
private array $cache = [];
public function __construct(private \Closure $batchFn) {}
public function add(int $id): void { $this->queue[$id] = true; }
public function loadOnce(): void {
$missing = array_diff(array_keys($this->queue), array_keys($this->cache));
if ($missing) {
foreach (($this->batchFn)(array_values($missing)) as $id => $row) {
$this->cache[$id] = $row;
}
}
$this->queue = [];
}
public function get(int $id): mixed { return $this->cache[$id] ?? null; }
}
In einen Resolver integrieren
Der author-Resolver stellt die ID in die Warteschlange und gibt ein Deferred zurück. graphql-php führt alle Deferreds nach der aktuellen Ebene aus. Wenn die Closure ausgeführt wird, wurden daher bereits alle Autoren-IDs der gesamten Liste in die Warteschlange gestellt. Ein Aufruf von loadOnce() löst eine einzige gebündelte Abfrage aus.
<?php
use GraphQL\Deferred;
$authorField = [
'type' => $userType,
'resolve' => function ($post, $args, $context) {
/** @var UserLoader $loader */
$loader = $context['userLoader'];
$loader->add($post['author_id']);
return new Deferred(function () use ($loader, $post) {
$loader->loadOnce(); // batches across all posts
return $loader->get($post['author_id']);
});
},
];
Die Bibliothek overblog/dataloader verwenden
In der Praxis implementieren Sie das nur selten selbst. overblog/dataloader-php ist der etablierte Port von Facebooks DataLoader. Sie übergeben eine Batch-Funktion, die ein Array von Schlüsseln erhält und ein Promise mit Werten in derselben Reihenfolge zurückgeben muss. Die Bibliothek übernimmt Caching, Deduplizierung und die Auflösung von Promises.
composer require overblog/dataloader-phpEinen DataLoader erstellen
Der Vertrag der Batch-Funktion ist strikt: Für [k1, k2, k3] muss sie positional zu [v1, v2, v3] aufgelöst werden. Indizieren Sie Ihre Datenbankzeilen nach Schlüssel und stellen Sie die Reihenfolge der Eingabe wieder her, sodass fehlende Schlüssel zu null werden.
<?php
use Overblog\DataLoader\DataLoader;
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
use Overblog\PromiseAdapter\Adapter\WebonyxGraphQLSyncPromiseAdapter;
$adapter = new WebonyxGraphQLSyncPromiseAdapter(new SyncPromiseAdapter());
$userLoader = new DataLoader(function (array $ids) use ($adapter, $db) {
$rows = $db->usersByIds($ids); // SELECT ... WHERE id IN (...)
$byId = [];
foreach ($rows as $r) { $byId[$r['id']] = $r; }
// MUST return values in the SAME ORDER as $ids
$ordered = array_map(fn($id) => $byId[$id] ?? null, $ids);
return $adapter->createFulfilled($ordered);
}, $adapter);
Über den Loader auflösen
Im Resolver rufen Sie einfach $loader->load($id) auf, das ein Promise zurückgibt. graphql-php sammelt diese über den Adapter und löst die Batch-Verarbeitung am Ende des Takts automatisch aus. Kein manuelles Puffern erforderlich.
<?php
$authorField = [
'type' => $userType,
'resolve' => fn($post, $args, $context) =>
$context['userLoader']->load($post['author_id']),
];
Die Lebensdauer pro Request ist entscheidend
DataLoader speichert Werte nach Schlüssel im Cache und muss daher für jeden Request neu erstellt werden. Ein Loader, der über mehrere Requests hinweg geteilt wird, würde veraltete Daten liefern und Speicherlecks verursachen. Erstellen Sie die Loader beim Zusammenstellen des Request-$context und verwerfen Sie sie, sobald der Request endet.
<?php
// Per request: brand new loaders, attached to context
function buildContext($db, $currentUser): array {
return [
'db' => $db,
'user' => $currentUser,
'userLoader' => makeUserLoader($db), // fresh, not a singleton
'postLoader' => makePostLoader($db),
];
}
Weitere Schutzmaßnahmen für die Performance
DataLoader behebt N+1-Lesezugriffe, aber eine bösartige oder unbedachte Abfrage kann Ihr System weiterhin belasten. Ergänzen Sie folgende Schutzmaßnahmen:
- Begrenzung der Abfragetiefe – die Regel
QueryDepthweist pathologisch tief verschachtelte Abfragen zurück. - Abfragekomplexität –
QueryComplexityweist jedem Feld ein Kostenbudget zu. - Persistierte Abfragen – erlauben Sie nur Vorgänge aus einer bekannten Allowlist.
- Paginierung – lösen Sie niemals unbegrenzte Listen auf, sondern verwenden Sie Cursor-Verbindungen.
<?php
use GraphQL\Validator\Rules\QueryDepth;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\DocumentValidator;
DocumentValidator::addRule(new QueryDepth(10));
DocumentValidator::addRule(new QueryComplexity(200));
Den Gewinn messen
Quantifizieren Sie die Verbesserung immer. Umschließen Sie Ihre Datenbankschicht in einem Test, um die Anzahl der Abfragen zu zählen, und stellen Sie anschließend sicher, dass die gebündelte Version unabhängig von der Listengröße eine begrenzte Anzahl von Abfragen ausführt. So verhindern Sie Regressionen, bei denen jemand einen naiven verschachtelten Resolver hinzufügt und N+1 unbemerkt wieder einführt.
<?php
class CountingDb {
public int $queries = 0;
public function usersByIds(array $ids): array {
$this->queries++; // one batched call
return array_map(fn($id) => ['id' => $id], $ids);
}
}
$db = new CountingDb();
$db->usersByIds([7, 9, 11, 13]); // 4 authors
echo "Queries for 4 authors: {$db->queries}\n"; // Queries for 4 authors: 1
Schnelltest
Warum müssen DataLoader pro Request erstellt werden?
Zusammenfassung
Sie haben GraphQLs schlimmste Performance-Falle beseitigt:
- Verschachtelte Listenfelder verursachen N+1: eine Abfrage pro Element.
- graphql-php löst Felder Ebene für Ebene auf. Durch Zurückstellen können Sie daher alle Schlüssel in einer
IN (...)-Abfrage bündeln. GraphQL\Deferredist das grundlegende Sprachmittel;overblog/dataloader-phpbündelt Batch-Verarbeitung, Caching pro Schlüssel und Deduplizierung.- Die Batch-Funktion muss Werte in derselben Reihenfolge wie die Eingabeschlüssel zurückgeben.
- Loader gelten pro Request. Ergänzen Sie außerdem Begrenzungen für Tiefe und Komplexität sowie Paginierung als weitere Schutzmaßnahmen.
Lerne PHP mit einem KI-Tutor — kostenlos
Schreibe und führe echten Code in deinem Browser aus, bekomme sofortige Hilfe von einem 24/7 KI-Tutor und setze dein Lernen im Web oder in der App fort.
- Kurse
- 49
- Lektionen
- 195
Häufig gestellte Fragen
Ist die Lektion „Performance: N+1 und DataLoader“ kostenlos?
Ja — der vollständige Text von „Performance: N+1 und DataLoader“ ist hier im Web kostenlos zu lesen. Um sie interaktiv zu üben (integrierter Code-Editor und 24/7 KI-Tutor) und den Rest des PHP Academy-Kurses freizuschalten, upgrade auf CoddyKit PRO. Der PHP Academy-Kurs umfasst insgesamt 4 Lektionen.
Was lerne ich in „Performance: N+1 und DataLoader“?
Feldauflösungen bündeln und cachen, damit alles schnell bleibt Du übst PHP Academy mit praktischem Code, den du direkt im Browser ausführst, und ein 24/7 KI-Tutor beantwortet deine Fragen während du die Lektion bearbeitest.
Brauche ich Erfahrung, um PHP Academy zu starten?
Keine Vorkenntnisse erforderlich. PHP Academy auf CoddyKit ist für Anfänger bis fortgeschrittene Lernende strukturiert, sodass du hier starten oder von Anfang an beginnen und in deinem eigenen Tempo voranschreiten kannst. Dies ist Lektion 4 von 4.
Wie lange dauert die Lektion „Performance: N+1 und DataLoader“?
Die meisten CoddyKit-Lektionen dauern etwa 5–10 Minuten. Jede ist kompakt und interaktiv, sodass du stetig Fortschritte machst und genau dort weitermachst, wo du aufgehört hast – im Web und in der App.
Kann ich in dieser PHP Academy-Lektion Code schreiben und ausführen?
Ja. Jede PHP Academy-Lektion enthält einen integrierten Code-Editor, sodass du echten Code direkt in deinem Browser schreibst und ausführst und sofort KI-Feedback erhältst — ohne lokale Einrichtung erforderlich.
Alle Lektionen in diesem Kurs
- GraphQL vs. REST
- Ein Schema mit graphql-php erstellen
- Resolver, Mutationen und Subscriptions
- Performance: N+1 und DataLoader