Was ist das N+1 Problem?
Das N+1 Problem ist ein klassisches Performance-Problem bei Datenbankabfragen. Es tritt auf, wenn für eine Liste von N Elementen zusätzlich N einzelne Abfragen gemacht werden.
Beispiel: Du hast 100 Bestellungen und willst für jede den Kunden laden.
// Schlecht: 1 Query für Bestellungen + 100 Queries für Kunden
const orders = await Order.findAll(); // 1 Query
for (const order of orders) {
order.customer = await Customer.findById(order.customerId); // 100 Queries
}Das sind 101 Datenbankabfragen statt 2.
Warum ist GraphQL besonders betroffen?
GraphQL Resolver werden unabhängig voneinander ausgeführt. Wenn du einen customer Resolver auf dem Order Type hast, wird dieser für jede Bestellung einzeln aufgerufen.
// GraphQL Schema
type Order {
id: ID!
customer: Customer! // Dieser Resolver wird N mal aufgerufen
}
// Resolver
const resolvers = {
Order: {
customer: (order) => Customer.findById(order.customerId)
}
}Lösung 1: DataLoader
DataLoader ist die Standard-Lösung. Es sammelt alle Anfragen innerhalb eines Ticks und führt sie als Batch aus.
import DataLoader from 'dataloader';
// Batch-Funktion: Lädt alle Kunden auf einmal
const customerLoader = new DataLoader(async (ids) => {
const customers = await Customer.findAll({
where: { id: ids }
});
// Wichtig: Rückgabe in der gleichen Reihenfolge wie die IDs
return ids.map(id => customers.find(c => c.id === id));
});
// Resolver verwendet den Loader
const resolvers = {
Order: {
customer: (order) => customerLoader.load(order.customerId)
}
}Jetzt werden alle Kunden mit einer einzigen Query geladen.
Lösung 2: Eager Loading
Wenn du weisst, dass du die Relations immer brauchst, kannst du sie direkt mit laden.
// Mit Prisma
const orders = await prisma.order.findMany({
include: { customer: true }
});
// Mit Sequelize
const orders = await Order.findAll({
include: [Customer]
});Lösung 3: Query Complexity Analysis
In manchen Fällen hilft es, die Query-Komplexität zu analysieren und entsprechend zu laden.
// Prüfen, ob 'customer' im Query angefragt wurde
const resolvers = {
Query: {
orders: async (_, __, ___, info) => {
const includeCustomer = hasField(info, 'customer');
return Order.findAll({
include: includeCustomer ? [Customer] : []
});
}
}
}Best Practices
- Immer DataLoader verwenden – Auch wenn du denkst, es ist nicht nötig. Es schadet nie.
- DataLoader pro Request – Erstelle für jeden Request einen neuen DataLoader. Sonst hast du Cache-Probleme.
- Monitoring – Logge die Anzahl Datenbankabfragen pro Request. N+1 Probleme fallen sofort auf.
- Query Depth Limiting – Begrenze die Tiefe von GraphQL Queries, um exzessive Datenbankabfragen zu vermeiden.
Fazit
Das N+1 Problem ist lösbar – aber du musst aktiv dagegen vorgehen. DataLoader ist dein Freund. Verwende es von Anfang an, nicht erst wenn die Performance-Probleme da sind.