Esnek sorgular güzel ama beraberinde gizli bir performans tuzağı getiriyor
Merhaba!
GraphQL'in esnekliği harika ama dikkatli tasarlanmazsa ciddi bir performans problemi yaratabiliyor: N+1 query problemi. Bu konuda bu klasik sorunu ve DataLoader ile çözümünü derinlemesine ele aldım.
Bir GraphQL sorgusu, ilişkili veri getirirken her bir öğe için AYRI bir veritabanı sorgusu tetiklerse bu N+1 problemi. "1" ana sorgu + "N" ilişkili veri sorgusu.
JavaScript:
// ❌ KÖTÜ — N+1 problemi yaratan resolver
const cozumleyiciler = {
Query: {
yazarlar: async () => {
return await Yazar.findAll(); // 1 sorgu — tüm yazarları getir
}
},
Yazar: {
kitaplar: async (yazar) => {
// ⚠️ HER YAZAR İÇİN AYRI SORGU!
// 100 yazar varsa bu 100 kez çalışır = 100 ayrı sorgu
return await Kitap.findAll({ where: { yazarId: yazar.id } });
}
}
};
// Şu sorgu için:
// query {
// yazarlar {
// isim
// kitaplar { baslik } ← Her yazar için ayrı sorgu tetikliyor!
// }
// }
// 100 yazar = 1 (yazarlar) + 100 (her yazarın kitapları) = 101 SORGU!
// ✅ İYİ — DataLoader ile batch (toplu) sorgulama
const DataLoader = require('dataloader');
function kitapYukleyiciOlustur() {
return new DataLoader(async (yazarIdleri) => {
// Tüm yazar ID'leri TEK BİR sorguda toplanıyor
console.log(`📦 Batch sorgu: ${yazarIdleri.length} yazar için TEK sorgu`);
const tumKitaplar = await Kitap.findAll({
where: { yazarId: yazarIdleri } // IN sorgusu — tek seferde
});
// Sonuçları, orijinal ID sırasına göre gruplandır
// DataLoader'ın beklediği format: her ID için bir dizi sonuç
return yazarIdleri.map(yazarId =>
tumKitaplar.filter(kitap => kitap.yazarId === yazarId)
);
});
}
const cozumleyicilerDogru = {
Query: {
yazarlar: async (_, __, context) => {
return await Yazar.findAll();
}
},
Yazar: {
kitaplar: async (yazar, _, context) => {
// DataLoader otomatik olarak bu çağrıları TOPLAR
// ve tek bir batch sorguda birleştiriyor!
return await context.kitapYukleyici.load(yazar.id);
}
}
};
// Her request için YENİ bir DataLoader instance'ı oluşturulmalı
// (context oluşturulurken):
function contextOlustur() {
return {
kitapYukleyici: kitapYukleyiciOlustur()
};
}
// Sonuç: 100 yazar = 1 (yazarlar) + 1 (TÜM kitaplar batch) = SADECE 2 SORGU!
DataLoader, aynı "event loop tick"i içinde yapılan tüm `.load()` çağrılarını topluyor (batching), sonra bunları TEK bir toplu sorguda birleştiriyor. Ayrıca varsayılan olarak sonuçları cache'liyor — aynı ID için tekrar sorgu yapmıyor.
SQL:
-- Veritabanı sorgu loglarınızı izleyin
-- Eğer tek bir GraphQL isteği için onlarca/yüzlerce benzer
-- sorgu görüyorsanız (sadece WHERE koşulundaki ID değişiyorsa),
-- bu klasik bir N+1 belirtisi
-- Örnek şüpheli log paterni:
-- SELECT * FROM kitaplar WHERE yazar_id = 1;
-- SELECT * FROM kitaplar WHERE yazar_id = 2;
-- SELECT * FROM kitaplar WHERE yazar_id = 3;
-- ... (100 kez tekrar) ...
GraphQL resolver'larında "ilişkili veri getiren" her fonksiyon, N+1 riski taşıyor olarak değerlendirilmeli. DataLoader (veya benzeri batching mekanizması) kullanmak, GraphQL API'lerinde neredeyse her zaman gerekli bir best practice.
GraphQL API'nizde performans sorunu yaşadınız mı? Nasıl çözdünüz?