Yazılar

Veritabanını Kilitleyen N+1 Query (Sorgu) Tuzağı Nedir? Prisma, Drizzle ve SQL ile Eager Loading Çözümü

Lokal bilgisayarda 5 kayıt varken milisaniyede çalışan kod, canlı ortamda 100 sipariş listelenirken neden veritabanını kilitler? ORM kullanırken en sık düşülen N+1 Query probleminin anatomisi ve Eager Loading (JOIN / IN) ile tek satırlık çözümü.

Furkan Özay20 Ekim 2026 · 4 dk okuma
czCZAY.DEV // EDITORIALTR
DATABASE INTERNALS

#Veritabanı · 4 DK OKUMA

FURKAN ÖZAYVOL. 2026 // #2C8A

Hemen her backend geliştiricinin kariyerinde en az bir kez yaşadığı klasik bir sahne vardır: Son siparişleri ve siparişi veren müşterinin adını listeleyen bir API uç noktası (endpoint) yazarsın. Kendi bilgisayarında (localhost) test veritabanında 4-5 kayıt varken sayfa 6 milisaniyede şimşek gibi açılır.

Kodu canlıya (production) alırsın. Gerçek kullanıcılar sayfaya girip 100 siparişi listelediği anda PostgreSQL sunucusunun CPU'su %100'e vurur, veritabanı bağlantı havuzu (connection pool) tıkanır ve sayfa 3-4 saniyede zor açılmaya başlar.

Kodda hiçbir sözdizimi hatası yoktur ama veritabanını gizlice boğan en meşhur performans tuzağına düşmüşsündür: N+1 Query (Sorgu) Problemi!

· · ·

1. N+1 Query Tuzağı Nasıl Oluşur?

Son 100 siparişi ve her siparişin sahibini getirmek için yazılan şu masum görünümlü koda bakalım:

typescript
// ❌ N+1 TUZAĞI: Siparişler için 1 sorgu + döngü içinde her sipariş için 100 ayrı sorgu!
const orders = await db.query("SELECT * FROM orders LIMIT 100"); // -> 1. Sorgu (1)
 
for (const order of orders) {
  // Döngünün her adımında veritabanına YENİ bir ağ isteği (network roundtrip) atılır!
  order.user = await db.query("SELECT * FROM users WHERE id = $1", [order.user_id]); // -> 100 Sorgu (N)
}

Gel basit bir matematik yapalım:

  • İlk satırda N adet siparişi çekmek için 1 sorgu attık (N = 100).
  • Ardından for döngüsünün içinde, 100 siparişin her birinin kullanıcısını bulmak için veritabanına 100 ayrı sorgu daha attık.
  • Toplam: Tek bir kişinin sayfayı yenilemesiyle veritabanına tam 100 + 1 = 101 ayrı SQL sorgusu gitti! (Sayfaya aynı anda 50 kişi girerse veritabanına saniyede 5.050 sorgu çarpar!).

Asıl Katil Sorgu Süresi Değil, Ağ Gecikmesidir (Network Roundtrip)

users tablosunda id üzerinde Primary Key indeksi olduğu için PostgreSQL tek bir sorguyu 0.2 milisaniyede çözebilir. Ancak uygulama sunucun ile veritabanı sunucun arasındaki her gidiş-dönüş (TCP Network Roundtrip) bulut ortamında ortalama 3 ile 5 milisaniye sürer:

  • 2 sorgu atarsan: 2 x 4ms = 8ms ağ beklemesi.
  • 101 sorgu atarsan: 101 x 4ms = 404ms sadece yolda geçen boş bekleme süresi!
· · ·

2. Çözüm: Eager Loading (JOIN veya WHERE id IN)

Veritabanına 100 kere kapıyı çalıp "12 numaralı kullanıcı kim?", "15 numaralı kullanıcı kim?" diye tek tek sormak yerine; tek seferde tüm siparişlerdeki kullanıcı ID'lerini toplayıp 1 veya 2 sorguda tüm veriyi çekmeye Eager Loading (İstekli Yükleme) denir.

Yöntem A: Saf SQL LEFT JOIN (Toplam 1 Sorgu)

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

Yöntem B: ORM'lerde include / with Kullanımı (Toplam 2 Sorgu)

Prisma, Drizzle ORM, Laravel (Eloquent) veya Django kullanıyorsan N+1 problemini çözmek kelimenin tam anlamıyla tek satırdır:

typescript
// ✅ PRISMA ÇÖZÜMÜ: `include` ile ilişkili veriyi tek seferde toplu çeker
const orders = await prisma.order.findMany({
  take: 100,
  include: {
    user: true, // <- Arka planda SELECT * FROM users WHERE id IN (...) çalıştırır!
  },
});
 
// ✅ DRIZZLE ORM ÇÖZÜMÜ: `with` kullanımı
const orders = await db.query.orders.findMany({
  limit: 100,
  with: {
    user: true,
  },
});

Bu kodu yazdığında ORM arka planda sadece şu 2 sorguyu çalıştırır:

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

Ve gelen kullanıcıları uygulama belleğinde (RAM) mikrosaniyeler içinde siparişlerle eşleştirir. 101 sorgu anında 2 sorguya düşer!

Uyarı: GraphQL ve İç İçe Component Tuzaklarına Dikkat!

N+1 problemi her zaman açık bir for döngüsü içinde görünmez. GraphQL resolver'larında veya React Server Components / Laravel Blade şablonlarında her satır bileşeninin kendi içinde order.getUser() çağırması (Lazy Loading) en sinsi N+1 kaynağıdır. Geliştirme ortamında ORM'inin SQL loglamasını (log: ['query']) mutlaka açık tut!

· · ·

İnteraktif N+1 Query Teşhis Aracı & Kısa Video

Kendi projende N+1 darboğazı olup olmadığını adım adım kontrol etmek için hazırladığım interaktif teşhis aracını hemen kullanabilirsin:

İnteraktif Yol Haritası & Araç

Bunu da oku