Performans optimizasyonu çoğu zaman samanlıkta iğne aramak gibi hissettirir. Ancak bazen en büyük kazanımlar, yaygın tek bir anti-pattern’i düzeltmekten gelir: N+1 sorgu problemi.
Problem
Bir e-spor platformu üzerinde çalışırken, turnuva listeleme sayfalarının 2-3 saniye sürdüğünü fark ettim. Gerçek zamanlı rekabetçi bir oyun platformu için bu kabul edilemezdi.
Profiler suçluyu ortaya çıkardı: tek bir sayfa yüklemesi için 150’den fazla veritabanı sorgusu yapıyorduk.
N+1 Nedir?
N+1 sorgu problemi, bir öğe listesi aldığınızda (1 sorgu), ardından her öğe için ilgili verileri ayrı ayrı aldığınızda (N sorgu) ortaya çıkar. Sinsidir çünkü doğru çalışır—sadece yavaş.
// Problemli kod
async function getTournaments() {
// Turnuvaları almak için 1 sorgu
const tournaments = await Tournament.find({ status: 'active' });
// Katılımcı sayılarını almak için N sorgu
for (const tournament of tournaments) {
tournament.participantCount = await Participant.countDocuments({
tournamentId: tournament._id
});
}
// Organizatör bilgisini almak için N sorgu daha
for (const tournament of tournaments) {
tournament.organizer = await User.findById(tournament.organizerId);
}
return tournaments;
}
50 turnuva ile bu kod 101 sorgu çalıştırır (1 + 50 + 50).
Çözüm: Aggregation Pipeline’ları
MongoDB’nin aggregation pipeline’ı bunu tek bir sorguda çözebilir:
async function getTournaments() {
return Tournament.aggregate([
{ $match: { status: 'active' } },
// participants koleksiyonu ile birleştir
{
$lookup: {
from: 'participants',
localField: '_id',
foreignField: 'tournamentId',
as: 'participants'
}
},
// Organizatör için users koleksiyonu ile birleştir
{
$lookup: {
from: 'users',
localField: 'organizerId',
foreignField: '_id',
as: 'organizerData'
}
},
// Sonucu dönüştür
{
$project: {
name: 1,
startDate: 1,
prizePool: 1,
participantCount: { $size: '$participants' },
organizer: { $arrayElemAt: ['$organizerData', 0] }
}
},
{ $sort: { startDate: -1 } },
{ $limit: 50 }
]);
}
Sonuç: 101 sorgu 1’e düştü. Yanıt süresi 2.3 saniyeden 180ms’ye düştü.
N+1’in Ötesinde: Stratejik İndeksleme
N+1 sorunlarını düzelttikten sonra, bir sonraki darboğaz sorgu yürütme süresiydi. MongoDB, indeksleri kullanmak yerine tüm koleksiyonları tarıyordu.
Eksik İndeksleri Belirleme
Sorgu yürütmesini analiz etmek için explain() kullanın:
const explanation = await Tournament.find({ status: 'active' })
.sort({ startDate: -1 })
.explain('executionStats');
console.log(explanation.executionStats);
// {
// totalDocsExamined: 15000, // Tüm belgeleri taradı!
// totalKeysExamined: 0, // İndeks kullanılmadı
// executionTimeMillis: 450
// }
Bileşik İndeksler Oluşturma
Filtreleyen ve sıralayan sorgular için bileşik indeksler şarttır:
// Yaygın sorgu kalıbı için indeks oluştur
tournamentSchema.index({ status: 1, startDate: -1 });
// İndeks eklendikten sonra:
// {
// totalDocsExamined: 50, // Sadece gerekli belgeler incelendi
// totalKeysExamined: 50, // İndeks kullanıldı
// executionTimeMillis: 3 // 150 kat daha hızlı!
// }
İndeks Stratejisi Kuralları
- Önce eşitlik eşleşmeleri:
=ile kullanılan alanları aralık alanlarından önce koyun - Sonra sıralama alanları:
sort()içinde kullanılan alanları dahil edin - En son aralık alanları:
$gt,$ltvb. ile kullanılan alanlar
// Sorgu için: { status: 'active', region: 'EU' } startDate'e göre sıralanmış
// Optimal indeks:
{ status: 1, region: 1, startDate: -1 }
Redis Önbellekleme Katmanı
Bazı veriler sık değişmez ama sürekli istenir. Redis önbellekleme katmanı eklemek %40 daha fazla iyileştirme sağladı:
import Redis from 'ioredis';
const redis = new Redis();
const CACHE_TTL = 300; // 5 dakika
async function getTournaments(filters: TournamentFilters) {
const cacheKey = `tournaments:${JSON.stringify(filters)}`;
// Önce önbelleği dene
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Önbellek kaçırma - veritabanını sorgula
const tournaments = await fetchFromDatabase(filters);
// Önbellekte sakla
await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(tournaments));
return tournaments;
}
// Veri değiştiğinde önbelleği geçersiz kıl
async function updateTournament(id: string, data: UpdateData) {
await Tournament.findByIdAndUpdate(id, data);
// İlgili önbellek anahtarlarını geçersiz kıl
const keys = await redis.keys('tournaments:*');
if (keys.length > 0) {
await redis.del(...keys);
}
}
Sorgu Performansını İzleme
Önleme tedaviden iyidir. Yavaş sorguları kullanıcıları etkilemeden yakalamak için izleme kurun:
import mongoose from 'mongoose';
// Yavaş sorguları logla (> 100ms)
mongoose.set('debug', (collectionName, method, query, doc, options) => {
const start = Date.now();
return function(err, result) {
const duration = Date.now() - start;
if (duration > 100) {
console.warn(`Yavaş sorgu tespit edildi:`, {
collection: collectionName,
method,
query,
duration: `${duration}ms`
});
}
};
});
Prodüksiyon için bu metrikleri izleme sisteminize (Datadog, New Relic, vb.) gönderin.
Sonuçlar
Bu optimizasyonları uyguladıktan sonra:
| Metrik | Önce | Sonra | İyileştirme |
|---|---|---|---|
| İstek başına sorgu | 150+ | 1-3 | %98 azalma |
| Ortalama yanıt süresi | 2.3s | 85ms | %96 daha hızlı |
| P95 yanıt süresi | 4.1s | 180ms | %96 daha hızlı |
| Lighthouse Performans | 45 | 92 | %65 iyileştirme |
Önemli Çıkarımlar
-
Optimize etmeden önce profil çıkarın: Gerçek darboğazları belirlemek için veritabanı profiler’ları ve APM araçlarını kullanın.
-
N+1 her yerde: Sonuçlar üzerinde döngü yapıp ek sorgular yaptığınızda, muhtemelen N+1 probleminiz vardır.
-
İndeksler isteğe bağlı değil: Gerçek veri hacimleriyle prodüksiyonda, eksik indeksler üstel yavaşlamalara neden olur.
-
Stratejik önbellekleyin: Her şeyin önbelleklenmesi gerekmez. Sık okunan, nadiren değişen verilere odaklanın.
-
Sürekli izleyin: Veri büyüdükçe performans zamanla düşer. Yavaş sorgular için uyarılar kurun.
Sonuç
Veritabanı performansı akıllı numaralarla ilgili değil—veritabanlarının nasıl çalıştığını anlamak ve kısıtlamalarına saygı göstermekle ilgili. %65 Lighthouse iyileştirmesi tek bir sihirli düzeltmeden değil, sistematik olarak verimsizlikleri ortadan kaldırmaktan geldi.
Her milisaniye önemli. Kullanıcılarınız—ve altyapı maliyetleriniz—size teşekkür edecek.