Öncelikle N+1 probleminin ne olduğunu açıklayalım, ardından çözüm yöntemlerine birlikte bakalım.
N+1 Problemi Nedir?
N+1 problemi, ilişkili veriler çekilirken veritabanına gereğinden fazla sorgu atılması sonucu ortaya çıkan bir performans sorunudur.
Bu durum:
- Gereksiz sayıda query çalıştırılmasına
- Response süresinin artmasına
- Veritabanı yükünün artmasına
- Büyük ölçekli sistemlerde maliyetin yükselmesine
neden olur.
Örnek Senaryo
Bir kurs sistemi olduğunu düşünelim:
- 100 öğrenci var
- Her öğrenci birden fazla kursa kayıt olabilir (one-to-many ilişki)
Tüm öğrencilerin kayıt olduğu kursları getirmek istiyoruz.
Naif (Hatalı) Yaklaşım
- Önce tüm öğrencileri çekeriz → 1 query
- Daha sonra her öğrenci için kursları ayrı ayrı çekeriz → N query
Eğer 100 öğrenci varsa:
1 + 100 = 101 query
Bu durum N+1 problemi olarak adlandırılır.
Öğrenci sayısı arttıkça query sayısı da lineer şekilde artar ve sistem ciddi şekilde yavaşlar.
N+1 Problemini Nasıl Çözeriz?
N+1 problemini şu yöntemlerle çözebiliriz:
-
JOINkullanmak -
Batch query yazmak
-
ORM / ODM tarafında eager loading kullanmak
- Mongoose →
populate - Sequelize →
include
- Mongoose →
Mongoose ile N+1 Problemi (ODM)
Model Yapısı
User Model
- id
- createdAt
Order Model
- id
- userId
- price
- createdAt
Order koleksiyonunda userId üzerinden kullanıcıya ait order’ları bulduğumuzu varsayalım.
❌ N+1 Üreten Yanlış Kullanım
static async getOrders() {
const orders = await OrderModel.find();
for (const order of orders) {
await order.populate('userId');
}
return orders;
}
Burada:
- Önce tüm order’lar çekilir.
- Daha sonra her order için ayrı bir
populateçalışır. - Her loop için yeni bir query atılır.
Sonuç: N+1 problemi oluşur.
✅ Doğru Kullanım (Eager Loading)
static async getOrders() {
const orders = await OrderModel.find()
.populate('userId');
return orders;
}
Bu şekilde:
- Tek seferde ilişkili veriler çekilir.
- Ekstra query oluşmaz.
- Performans korunur.
Sequelize ile N+1 Problemi (ORM)
Model Yapısı
User
- id
- created_at
Order
- id
- user_id
- price
- created_at
❌ N+1 Üreten Yanlış Kullanım
static async getOrders() {
const users = await UserModel.findAll();
for (const user of users) {
const orders = await OrderModel.findAll({
where: { userId: user.id }
});
return orders;
}
}
Burada:
- Önce tüm kullanıcılar çekilir.
- Her kullanıcı için ayrı query atılır.
Yine klasik 1 + N problemi oluşur.
✅ Doğru Kullanım (Include ile Eager Loading)
static async getOrders() {
const users = await UserModel.findAll({
include: [
{
model: Order
}
]
});
}
Bu yapı arka planda JOIN üretir ve tek sorguda ilişkili verileri getirir.
SQL ile N+1 Problemi
ORM'lerde nasıl çözdüğümüzü gördük. Şimdi SQL seviyesinde bakalım.
100 kullanıcı ve her kullanıcıya ait 100 order olduğunu düşünelim.
❌ N+1 Yaklaşımı
Eğer:
- Önce kullanıcıları çekip
- Her kullanıcı için ayrı ayrı
SELECT * FROM orders WHERE user_id = ?
şeklinde sorgu atarsak yine N+1 problemi oluşur.
Bu yaklaşım:
- Büyük veri setlerinde ciddi performans kaybına
- Yüksek CPU ve I/O kullanımına
- Maliyet artışına
neden olur.
✅ Doğru Yaklaşım: JOIN Kullanmak
SELECT
u.id,
u.name,
o.id,
o.total_price
FROM users u
JOIN orders o ON o.user_id = u.id
LIMIT 0, 10000;
Bu sorgu:
- Tek query ile tüm ilişkili veriyi getirir.
- Çok daha hızlıdır.
- Büyük veri setlerinde daha ölçeklenebilirdir.
Örnek sonuç:
10000 row(s) returned
0.0019 sec / 0.018 sec
N+1 problemi:
- Küçük projelerde fark edilmeyebilir.
- Ancak ölçek büyüdükçe sistem performansını ciddi şekilde düşürür.
- Özellikle mikroservis mimarilerinde ve yüksek trafikli sistemlerde kritik hale gelir.
Unutulmaması gereken nokta şudur:
İlişkili veri çekiyorsanız, her zaman tek query ile çözebilir miyim diye düşünmelisiniz.
JOIN, eager loading ve doğru veri modeli tasarımı bu problemin temel çözümleridir.