Das N+1 Problem in GraphQL

Was ist das N+1 Problem und wie löst man es in GraphQL?

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: Sie haben 100 Bestellungen und wollen 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 Sie einen customer Resolver auf dem Order Type haben, 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 Sie wissen, dass Sie die Relations immer brauchen, können Sie 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 Sie denken, es ist nicht nötig. Es schadet nie.
  • DataLoader pro Request – Erstellen Sie für jeden Request einen neuen DataLoader. Sonst haben Sie Cache-Probleme.
  • Monitoring – Loggen Sie die Anzahl Datenbankabfragen pro Request. N+1 Probleme fallen sofort auf.
  • Query Depth Limiting – Begrenzen Sie die Tiefe von GraphQL Queries, um exzessive Datenbankabfragen zu vermeiden.

Fazit

Das N+1 Problem ist lösbar – aber Sie müssen aktiv dagegen vorgehen. DataLoader ist Ihr Freund. Verwenden Sie es von Anfang an, nicht erst wenn die Performance-Probleme da sind.

GraphQL Performance-Probleme?

Wir optimieren Ihre GraphQL API.

Kontakt aufnehmen