Artikel

Der Datenbank-Killer: Was ist das N+1-Query-Problem und wie behebst du es mit Eager Loading

Code, der auf deinem Laptop mit 3 Testzeilen in 5 ms läuft, kann beim Auflisten von 100 Bestellungen leicht deinen PostgreSQL-Server in der Produktion lahmlegen. So entsteht die N+1-Query-Falle in modernen ORMs und so behebst du sie mit einer Zeile.

Furkan Özay20. Oktober 2026 · 3 Min. Lesezeit
czCZAY.DEV // EDITORIALDE
DATABASE INTERNALS

#Veritabanı · 3 MIN LESEZEIT

FURKAN ÖZAYVOL. 2026 // #2C8A

Jeder Backend-Entwickler hat dieses Rätsel schon mindestens einmal erlebt: Du baust einen API-Endpoint, der die letzten Bestellungen samt Namen des jeweiligen Kunden auflistet. Auf deinem lokalen Entwicklungsrechner mit 4 Seed-Datensätzen antwortet der Endpoint in rasanten 6 Millisekunden.

Du deployst in die Produktion. Sobald Traffic reinkommt und die Seite pro Anfrage 100 Bestellungen rendert, schnellt die CPU-Auslastung deines PostgreSQL-Servers auf 100 %, die Connection Pools laufen leer und die Antwortzeiten kriechen auf 3,5 Sekunden.

Der Schuldige? Das berüchtigtste Performance-Antipattern der ORM-Geschichte: das N+1-Query-Problem.

· · ·

1. Anatomie einer N+1-Query

Sieh dir diesen scheinbar harmlosen Code an, der die letzten 100 Bestellungen lädt und jeweils die Kundendaten anhängt:

typescript
// ❌ THE N+1 TRAP: 1 query for orders + 100 separate queries inside the loop!
const orders = await db.query('SELECT * FROM orders LIMIT 100'); // -> 1st Query (1)
 
for (const order of orders) {
  // Executes a brand-new network roundtrip to PostgreSQL on EVERY iteration!
  order.user = await db.query('SELECT * FROM users WHERE id = $1', [order.user_id]); // -> 100 Queries (N)
}

Rechnen wir nach:

  • 1 Query lädt die Liste der N Bestellungen (N = 100).
  • 100 einzelne Queries laden den Nutzer zu jeder einzelnen Bestellung.
  • Gesamt: 100 + 1 = 101 Datenbankabfragen für eine einzige HTTP-Anfrage!

Warum dich die Latenz der Netzwerk-Roundtrips umbringt

Selbst wenn PostgreSQL jedes einzelne SELECT * FROM users WHERE id = $1 dank Primärschlüssel-Index in 0.2ms ausführt, braucht jede einzelne Query einen TCP-Roundtrip zwischen deinem API-Container und deinem Datenbankserver (~3ms bis 5ms in Cloud-VPCs).

  • 2 queries × 4ms network overhead = 8ms
  • 101 queries × 4ms network overhead = 404ms (für nur einen Nutzer, der die Seite neu lädt!)
· · ·

2. Die Lösung: Eager Loading (JOIN oder WHERE IN)

Statt der Datenbank 100 einzelne Fragen nacheinander zu stellen, holt Eager Loading alle benötigten Daten mit insgesamt 1 oder 2 Queries.

Variante A: SQL LEFT JOIN (insgesamt 1 Query)

SQL
SELECT orders.*, users.name, users.email
FROM orders
LEFT JOIN users ON orders.user_id = users.id
LIMIT 100;

Variante B: Batch WHERE id IN (...) (insgesamt 2 Queries)

Moderne ORMs wie Prisma, Drizzle, Eloquent (Laravel) und Django aktivieren Eager Loading mit einer einzigen Eigenschaft:

typescript
// ✅ PRISMA: Uses `include` to batch-fetch all related users in 1 extra query (WHERE id IN (...))
const orders = await prisma.order.findMany({
  take: 100,
  include: {
    user: true, // <- Eliminates the N+1 loop!
  },
});
 
// ✅ DRIZZLE ORM: Relational query with `with`
const orders = await db.query.orders.findMany({
  limit: 100,
  with: {
    user: true,
  },
});

Unter der Haube führt das ORM Folgendes aus:

  1. SELECT * FROM orders LIMIT 100;
  2. SELECT * FROM users WHERE id IN (12, 15, 19, 44, ...);

Und setzt die Objekte in Mikrosekunden im Speicher zusammen! Aus 101 Queries werden 2 Queries.

· · ·

Interaktives Diagnose-Tool

Du willst deine eigenen ORM-Muster testen und unsere Schritt-für-Schritt-Checkliste zur N+1-Erkennung durchgehen?

👉 Starte das interaktive N+1-Query-Diagnose-Tool

Interaktive Roadmap & Tool

Als Nächstes lesen