Furkan KapukayaYazılım geliştirme
Backend, API ve Veritabanı3 dk okuma

N+1 Sorgu Problemi: ORM Kullanırken En Sık Yapılan Performans Hatası

N+1 sorgu probleminin ne olduğu, Entity Framework Core'da nasıl ortaya çıktığı, tespit yöntemleri ve Include, projeksiyon, split query ve toplu yükleme ile çözümü.

Geliştirme ortamında on kayıtla hızlı çalışan bir sayfa, canlıda bin kayıtla dakikalarca açılıyorsa ilk şüpheli çoğu zaman N+1 problemidir. Bir liste için 1 sorgu, listedeki her öğe için de ayrı birer sorgu çalıştırılır: toplam N+1 sorgu.

Nasıl ortaya çıkar?

var siparisler = await db.Siparisler.ToListAsync();          // 1 sorgu

foreach (var s in siparisler)
{
    var musteriAdi = s.Musteri.Ad;   // lazy loading açıksa her döngüde 1 sorgu
}

500 sipariş varsa veritabanına 501 kez gidilir. Her gidişin ağ gecikmesi küçük görünse de toplamda büyük bir yük oluşur. Aynı sorun, döngü içinde servis veya repository metodu çağırırken de yaşanır.

Nasıl tespit edilir?

  • EF Core'un SQL loglarını geliştirme ortamında açın: aynı sorgunun farklı parametrelerle tekrar tekrar çalıştığını göreceksiniz. Bkz. .NET'te loglama.
  • Gözlemlenebilirlik araçlarında bir isteğin altında onlarca veritabanı çağrısı görünür. Bkz. gözlemlenebilirlik.
  • Sayfa süresinin kayıt sayısıyla doğrusal arttığını fark etmek de güçlü bir ipucudur.

Çözümler

1. Projeksiyon (en iyi çözüm çoğu zaman bu)

Yalnızca ihtiyaç duyulan alanları tek sorguda alın:

var liste = await db.Siparisler
    .Select(s => new SiparisListeDto(s.Id, s.Tarih, s.Tutar, s.Musteri.Ad))
    .ToListAsync();

Tek bir JOIN sorgusu oluşur ve gereksiz sütunlar taşınmaz.

2. Include ile birlikte yükleme

var siparisler = await db.Siparisler
    .Include(s => s.Musteri)
    .ToListAsync();

3. Split query

Birden fazla koleksiyonu birlikte yüklerken tek dev JOIN, satır sayısını patlatabilir ("kartezyen patlama"). AsSplitQuery() her koleksiyonu ayrı ama sabit sayıda sorguyla getirir.

4. Toplu yükleme

Döngüden önce gereken tüm kayıtları tek sorguda alıp sözlüğe koyun:

var idler = siparisler.Select(s => s.MusteriId).Distinct().ToList();
var musteriler = await db.Musteriler.Where(m => idler.Contains(m.Id))
                                    .ToDictionaryAsync(m => m.Id);

Koleksiyon seçimi için bkz. C# koleksiyonları.

Lazy loading hakkında

Lazy loading, ilişkili veriye erişildiği anda otomatik sorgu çalıştırır. Rahat görünür ama N+1 probleminin en yaygın kaynağıdır. Birçok ekip onu kapalı tutar ve veriyi açıkça yüklemeyi tercih eder.

Diğer ilgili ipuçları

  • Salt okunur sorgularda AsNoTracking() kullanın.
  • Liste sayfalarında sayfalama yapın; binlerce kaydı tek seferde çekmeyin.
  • Sık kullanılan filtre ve JOIN sütunlarına indeks ekleyin.

EF Core temelleri için bkz. Entity Framework Core nedir.

Sık sorulan sorular

N+1 sadece EF Core'da mı görülür?

Hayır. Tüm ORM'lerde ve hatta elle yazılmış kodda, döngü içinde veri erişimi yapıldığında ortaya çıkar. GraphQL API'lerde de sık görülür.

Include her zaman doğru çözüm mü?

Hayır. Gereksiz alanları ve ilişkileri yükleyebilir. Liste ve raporlarda projeksiyon genellikle daha verimlidir.

Sonuç

N+1 problemi, küçük veride görünmez, büyük veride sistemi kilitler. Sorguları loglamak, projeksiyonla gerekeni tek seferde almak ve döngü içinde veri erişiminden kaçınmak; ORM kullanırken en önemli performans alışkanlıklarıdır.