EF Core’da performans kazanımları çoğunlukla üç alandan gelir: gereksiz veri çekimini azaltmak, Change Tracker yükünü düşürmek ve sorgu derleme/çalıştırma maliyetlerini optimize etmek. Aşağıdaki pratikler, okuma ve yazma trafiğinde somut hızlanma sağlar.

efcore

Giriş: Neyi optimize ediyoruz?

  • Okuma tarafında, LINQ sorgularının ürettiği SQL’in az veri çekmesi ve doğru indekslerle çalışması kritik önemdedir. Ayrıca izleme maliyetini gereksiz yere yüklememek gerekir.
  • Yazma tarafında, tekrarlı round‑trip’leri azaltmak, toplu işlemleri tercih etmek ve SaveChanges stratejisini doğru seçmek önemlidir.
  • Sorgu derleme maliyetini sıcak yollar için minimize etmek, yüksek trafikte CPU kazancı sağlar.(Burada kast edilen EF Core’un LINQ sorgusunu SQL’e çevirip delegate olarak derleme süreci. SQL Server’ın execution plan cache’i ile karıştırmamak gerekir.)

Change Tracker’ı dozunda kullan

  • Okuma senaryolarında AsNoTracking() ile izlemeyi kapat; salt okunur listeler için çoğu zaman yeterlidir.
  • Navigasyonları çoklayan referansları tekilleştirmek gerektiğinde AsNoTrackingWithIdentityResolution() kullan; bellek ve CPU maliyeti biraz artar ama tekrarları önler.
  • Web isteklerinde kısa ömürlü DbContext tercih edilir. UI uygulamalarında (Blazor Server, WPF gibi) uzun ömürlü context de kullanılabilir; senaryoya göre karar vermek gerekir; gereksiz attach/track’leri önle ve büyük isteklerde izleme alanını daralt.

Kod:

// Salt okunur
var posts = await context.Posts.AsNoTracking().ToListAsync();

// Tekilleştirme gerektiğinde
var postsWithTags = await context.Posts
    .AsNoTrackingWithIdentityResolution()
    .Include(p => p.Tags)
    .ToListAsync();

Projeksiyon: Gerekeni çek, fazlasını değil

  • Include zincirleri gereksiz join’lere ve tekrar eden satırlara yol açabilir (sonuç seti şişer). DTO/anonim tip projeksiyonu tercih et.
  • Sadece UI’nin ihtiyaç duyduğu alanları çek; ağ trafiği ve materialization süresi küçülür.

Kod:

var list = await context.Products
    .AsNoTracking()
    .Where(p => p.IsActive)
    .Select(p => new ProductListItemDto {
        Id = p.Id, Name = p.Name, Price = p.Price
    })
    .ToListAsync();

efcore

Compiled queries: sıcak yolları derleyip tekrar kullan

  • EF, LINQ sorgularını ilk çalıştırmada derler; yüksek trafikli, parametreli ve sık çağrılan sorgular EF.CompileQuery/EF.CompileAsyncQuery ile önceden derlenip yeniden kullanılabilir.
  • Parametre düzenini stabil tut; query plan/önbellek israfını azaltırsın.
  • CompileAsyncQuery kendi asenkron wrapper’ını sağladığı için FirstOrDefault kullanımı doğrudur. Alternatif olarak SingleOrDefault da kullanılabilir.
  • EF Core 7 ve sonrası bazı basit sorgular için otomatik cache kullanıyor; compiled query özellikle yüksek trafikli ve karmaşık sorgularda fayda sağlar.

Kod:

static readonly Func<AppDbContext, int, Task<Product?>> GetByIdAsync =
    EF.CompileAsyncQuery((AppDbContext db, int id) =>
        db.Products.Where(p => p.Id == id).FirstOrDefault());

var product = await GetByIdAsync(context, 42);

Sorgu şekillendirme: filtre, sıralama, sayfalama

  • Filtreyi erken uygula, sadece gerektiğinde OrderBy ekle; gereksiz sıralama pahalıdır.
  • Her zaman Take/Skip ile sayfalama uygula; UI listelerinde tüm tabloyu çekmekten kaçın.
  • LINQ yazımını, veritabanı indeksleriyle uyumlu hale getir; execution plan’ı gözlemleyerek Where/Join sırasını optimize et.

Kod:

var page = await context.Orders
    .AsNoTracking()
    .Where(o => o.Status == OrderStatus.Completed)
    .OrderByDescending(o => o.CreatedAt)
    .Skip((pageIndex - 1) * pageSize)
    .Take(pageSize)
    .Select(o => new OrderRow { Id = o.Id, Total = o.Total })
    .ToListAsync();

Yazma yükleri: ExecuteUpdate/ExecuteDelete ve SaveChanges stratejisi

  • Çok satırlı güncellemelerde ExecuteUpdate/ExecuteDelete kullan; round‑trip ve materialization maliyetini düşür.(EF Core 7+ ile desteklenir, kullanılan veritabanı provider’ına bağlıdır.)
  • SaveChangesAsync() özellikle I/O-bound senaryolarda (ör. DB gecikmesi yüksek olduğunda) ölçeklenebilirlik sağlar. CPU-bound küçük işlemlerde fark minimal olabilir.

Kod:

// Toplu güncelleme
await context.Products
    .Where(p => p.IsActive && p.Stock == 0)
    .ExecuteUpdateAsync(s => s.SetProperty(p => p.IsActive, false));

// Toplu silme
await context.Logs
    .Where(l => l.CreatedAt < DateTime.UtcNow.AddMonths(-3))
    .ExecuteDeleteAsync();

Change Tracker ince ayarları ve grafik güncellemeleri

  • Büyük grafik güncellemelerinde, sadece değişen alanları SetValues/Update ile hedefle; tüm grafiği track etme.
  • Detach ile işin biten entity’leri izleme dışına çıkar; uzun yaşayan context’lerde bellek baskısını azalt.
  • AutoDetectChangesEnabled’i yoğun döngülerde geçici olarak kapatıp, bitince aç; ölçerek uygula.
  • Daha ileri seviye optimizasyon için SaveChanges(false) + AcceptAllChanges() paterni de kullanılabilir; böylece her SaveChanges çağrısında DetectChanges maliyetini azaltırsın.

Kod:

context.ChangeTracker.AutoDetectChangesEnabled = false;
try
{
    foreach (var item in bulkItems)
        context.Entry(item).State = EntityState.Modified;

    await context.SaveChangesAsync();
}
finally
{
    context.ChangeTracker.AutoDetectChangesEnabled = true;
}

Önbellek ve okuma optimizasyonu

  • Sık okunan küçük referans verileri için uygulama içi cache (MemoryCache) veya dağıtık cache (Redis) kullan; cache invalidation politikasını netleştir.
  • Okuma ağırlıklı senaryolarda read/write ayrımı uygula; raporlama sorgularını replica üzerinde koşturmayı değerlendir.
  • EF Core’un NHibernate gibi bir second-level cache’i yok; cache için MemoryCache, Redis gibi harici çözümler kullanılmalı.

Dapper hibrit yaklaşımı

  • Aşırı sıcak ve karmaşık SELECT’lerde Dapper ile micro‑ORM yaklaşımı, EF ile domain yazma/izleme avantajlarını korurken okuma tarafında ekstra performans sağlar.
  • Aynı endpoint içinde EF ve Dapper karıştırmaktan kaçın; katmanlarda net sınırlar belirle.(Dapper dönüş tipleri EF entity’lerinden ayrı tutulmalı; aksi halde EF’de tracking/attach hataları olabilir.)

Ölç, doğrula, yinele

  • EF Core loglarını aç, oluşturulan SQL’i ve execution plan’ı incele; N+1 ve gereksiz Include’ları avla.
  • BenchmarkDotNet ile senaryo bazlı mikro‑benchmark yap; gerçekçi veri boyutu ve indekslerle ölç(EF Core sorgularında ilk çağrı her zaman derleme maliyeti taşır; warm-up sonrası ölçüm almak daha gerçekçi sonuç verir.).
  • Değişiklikten önce/sonra latency ve CPU profillerini kaydet; tahmin değil, ölçümle karar ver.

Sık yapılan hatalar

  • Varsayılan Include alışkanlığı; önce Select/DTO düşün, Include’u istisna yap.
  • Tüm listeler track ediliyor; AsNoTracking çoğu listede varsayılanın olmalı.
  • Büyük SaveChanges aralıkları; küçük ve anlamlı batch’ler ile transaction süresini kısalt.
  • Uzun yaşayan DbContext ve şişen tracker; scope ve yaşam döngüsünü kısalt, gereksiz attach’ten kaçın.

SSS

AsNoTracking ne zaman kullanılır?
Salt okunur listeler ve raporlamada; güncelleme yapmayacaksan izlemeyi kapatmak en düşük ek yükü verir.

Compiled queries her yerde gerekli mi?
Hayır; sadece yüksek trafikli ve parametreli “sıcak” yollar için anlamlı kazanç sağlar.EF Core 8’de bazı sorgular otomatik olarak daha verimli cache’lenir; versiyona göre kazanç değişebilir.

ExecuteUpdate/ExecuteDelete güvenli mi?
Evet, transaction içinde ve doğru filtrelerle kullanıldığında round‑trip ve materialization’ı azaltır; veri bütünlüğü için indeks ve kısıtları göz önünde bulundur.

N+1 problemini nasıl yakalarım?
Log seviyesini yükselt, SQL’leri izle; aynı endpoint içinde tekrar eden küçük sorgular görürsen uygun yerde Include veya projeksiyonla tek sorguya indir.

Dapper’ı ne zaman düşünmeli?
Sadece performans açısından kritik, karmaşık okuma sorgularında; yazma ve domain tarafında EF’nin izleme/ilişki avantajlarını koru.

By tanju.bozok

Software Architect, Developer, and Entrepreneur

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir