Blog'a dön

N+1 Sorgu Problemini Çözme: %65 Performans Artışı

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ı

  1. Önce eşitlik eşleşmeleri: = ile kullanılan alanları aralık alanlarından önce koyun
  2. Sonra sıralama alanları: sort() içinde kullanılan alanları dahil edin
  3. En son aralık alanları: $gt, $lt vb. 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ÖnceSonraİyileştirme
İstek başına sorgu150+1-3%98 azalma
Ortalama yanıt süresi2.3s85ms%96 daha hızlı
P95 yanıt süresi4.1s180ms%96 daha hızlı
Lighthouse Performans4592%65 iyileştirme

Önemli Çıkarımlar

  1. Optimize etmeden önce profil çıkarın: Gerçek darboğazları belirlemek için veritabanı profiler’ları ve APM araçlarını kullanın.

  2. N+1 her yerde: Sonuçlar üzerinde döngü yapıp ek sorgular yaptığınızda, muhtemelen N+1 probleminiz vardır.

  3. İndeksler isteğe bağlı değil: Gerçek veri hacimleriyle prodüksiyonda, eksik indeksler üstel yavaşlamalara neden olur.

  4. Stratejik önbellekleyin: Her şeyin önbelleklenmesi gerekmez. Sık okunan, nadiren değişen verilere odaklanın.

  5. 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.