ENTİTY FRAMEWORK CORE PERFORMANS: TRACKİNG, PROJECTİON VE N+1 SORUNU
Uygulamanız büyüdükçe “veritabanı yavaş” cümlesinin ardında çoğu zaman EF Core’un nasıl kullanıldığı yatar. Aynı tabloya tekrar tekrar gidilmesi, gereksiz tracking, yanlış projection ve fark edilmesi zor N+1 sorguları; saniyeler içinde hissedilen gecikmelere dönüşebilir.
Bu yazıda EF Core performans açısından en kritik üç başlığı—tracking, projection ve N+1 sorunu—birbirine bağlayarak ele alacağız. Amaç; “şu ayarı açınca hızlanır” gibi ezberler değil, sorgu davranışını okuyabilmek ve doğru yerde doğru optimizasyonu uygulamak.
Örnekler üzerinden ilerlerken, hangi durumda değişiklik izleme gereksizdir, hangi durumda projection kaçınılmazdır, N+1 nasıl ortaya çıkar ve nasıl önlenir gibi soruların net cevaplarını bulacaksınız.

EF Core Performansını Etkileyen Temel Dinamikler
EF Core, LINQ ifadelerinizi SQL’e çevirirken yalnızca “sorguyu çalıştırmakla” kalmaz; sonucu nesne grafiğine dönüştürür, ilişkileri çözer, gerektiğinde takip eder ve değişiklikleri kaydetmeye hazır hale getirir. Bu pipeline’ın her aşamasında bir maliyet vardır.
Performansı doğru ölçebilmek için üç katmanı ayırmak faydalıdır: veritabanı tarafı (indeks, plan, I/O), ağ/aktarım (taşınan kolon sayısı, sayfa büyüklüğü) ve uygulama tarafı (materialization, tracking, ilişki çözümü). Problemi doğru katmana yerleştirmek, çözümü hızlandırır.
Ölçmeden İyileştirme Olmaz: Basit bir yaklaşım
Önce “hangi endpoint ne kadar sürüyor” yerine “hangi sorgu ne kadar sürüyor” sorusunu cevaplayın. Log’larda SQL’i görün, süreleri kaydedin, aynı sorguyu farklı seçeneklerle (tracking/no-tracking, projection, include) kıyaslayın.
DbContext yaşam döngüsü ve birikimli maliyetler
Uzun yaşayan bir DbContext, Change Tracker içinde biriken entity’lerle zamanla yavaşlar. Bu, özellikle arka planda çok sayıda okuma yapan servislerde sinsi bir problemdir. Kısa ömürlü context kullanımı ve net sınırlar performans kadar doğruluk için de önemlidir.
Tracking Nedir ve Neden Pahalıdır?
Tracking, EF Core’un gelen entity’leri Change Tracker’a kaydetmesi ve sonradan değişiklik olup olmadığını izleyebilmesidir. Bu sayede SaveChanges sırasında hangi alanların güncelleneceğini bilir. Ancak her entity’nin izlenmesi; bellek, CPU ve ilişki çözümleme maliyeti demektir.
Salt okuma senaryolarında tracking çoğu zaman gereksizdir. Örneğin bir liste ekranında sadece görüntüleme yapıyorsanız, Change Tracker’ın “bu entity değişti mi” sorusunu sormasına ihtiyacınız yoktur.
AsNoTracking ve doğru kullanım sınırı
AsNoTracking ile sorgu sonucundaki entity’ler izlenmez; materialization daha hafif olur. Fakat dikkat: Sonuç üzerinde değişiklik yapıp kaydetmeyi planlıyorsanız no-tracking sizi yanıltabilir. Bu durumda ya tracking’li sorgu kullanın ya da güncelleme için ayrı bir akış tasarlayın.
Identity Resolution: Aynı entity’yi iki kez üretme riski
No-tracking sorgularda ilişkili verilerde aynı satır birden fazla kez dönerse aynı entity’nin birden fazla örneği oluşabilir. EF Core bazı senaryolarda identity resolution sağlayan seçenekler sunar; ancak bu da ek maliyet getirir. Okuma ekranlarında “tekil nesne kimliği” zorunlu değilse, en hafif yolu tercih edin.
// Okuma senaryosu: tracking kapalı
var products = await db.Products
.AsNoTracking()
.Where(p => p.IsActive)
.OrderByDescending(p => p.CreatedAt)
.Take(50)
.ToListAsync();
// Güncelleme senaryosu: tracking açık (veya hedefli attach)
var product = await db.Products
.FirstAsync(p => p.Id == id);
product.Price = newPrice;
await db.SaveChangesAsync();Projection: Daha Az Veri, Daha Az İş
Projection, entity’nin tamamını çekmek yerine sadece gerekli alanları seçmektir. “İşe yarar kolonları” taşımak hem ağ trafiğini hem de EF Core’un nesne üretim maliyetini düşürür. Özellikle geniş tablolar, büyük text alanları veya çok kolonlu join’lerde fark dramatik olabilir.
Projection aynı zamanda API sözleşmenizi sadeleştirir: UI’nın ihtiyacı olan DTO’yu üretirsiniz. Bu, performansın yanında güvenlik açısından da iyidir; istemeden hassas kolonların taşınmasını engeller.
Entity yerine DTO üretmek
Liste ekranları için çoğu zaman entity’ye ihtiyacınız yoktur. Bir “kart görünümü” veya “grid satırı” için gereken alanları seçin. Ayrıca gereksiz navigasyonları çekmediğiniz için N+1’e giden yolu da daraltırsınız.
// Projection ile sadece gereken alanlar
var items = await db.Orders
.AsNoTracking()
.Where(o => o.CreatedAt >= fromDate)
.OrderByDescending(o => o.CreatedAt)
.Select(o => new OrderListItemDto
{
Id = o.Id,
Number = o.Number,
CustomerName = o.Customer.Name,
Total = o.Lines.Sum(l => l.UnitPrice * l.Quantity),
CreatedAt = o.CreatedAt
})
.Take(100)
.ToListAsync();Ne zaman projection yapmamak daha iyidir?
Çok karmaşık projeksiyonlar, özellikle yanlış kurgulanırsa SQL tarafında ağır ifadeler üretebilir. Hesaplamayı veritabanına taşımak her zaman iyi değildir. Kural basit: veritabanının güçlü olduğu işi (filtreleme, sıralama, agregasyon) veritabanında; uygulamanın güçlü olduğu işi (formatlama, sunum odaklı dönüşüm) uygulamada yapın.
N+1 Sorunu: Sessiz Performans Katili
N+1, bir listeyi çektiğinizde her satır için ayrı bir ek sorgu çalışmasıdır. İlk sorgu “N öğeyi” getirir; ardından her öğe için bir sorgu daha çalışır ve toplamda 1 + N sorgu oluşur. Küçük veri setlerinde fark edilmez; gerçek trafikte patlar.
N+1 genellikle navigasyon property’lere erişirken (özellikle lazy loading açıkken) veya döngü içinde ilişki çağırırken ortaya çıkar. En tehlikeli tarafı; geliştirici makinesinde “normal görünen” bir akışın prod’da çok yavaşlamasıdır.
N+1 nasıl tespit edilir?
En pratik yöntem, ilgili endpoint’i çalıştırırken SQL log sayısını izlemektir. Bir liste endpoint’inde 1–2 sorgu beklerken onlarca sorgu görüyorsanız N+1 adaydır. Ayrıca APM araçlarında “çok sayıda benzer SELECT” paterni de tipiktir.
Çözüm stratejileri: Include, explicit loading, projection
N+1’i çözmenin üç ana yolu vardır. (1) Include/ThenInclude ile gerekli ilişkileri tek seferde getirmek, (2) explicit loading ile kontrollü şekilde toplu yüklemek, (3) projection ile ihtiyaç duyulan alanları join üzerinden DTO’ya almak. En doğru seçenek; ekranın veri ihtiyacına ve ilişki derinliğine göre değişir.
- Basit ilişki ve sınırlı alan: Projection genellikle en hızlısıdır.
- Geniş nesne grafiği ama az satır: Include makul olabilir.
- Çok satır ve çok ilişki: SplitQuery veya kontrollü explicit loading daha güvenlidir.
Include, SplitQuery ve Kartesyen Patlama Riski
Include kullanımı N+1’i çözebilir ama her zaman “tek sorgu” daha iyi değildir. Birden fazla koleksiyon navigasyonu Include ederseniz join sonucu satır sayısı katlanabilir ve veri tekrarları artar. Bu durum, bellek tüketimini yükseltir ve materialization maliyetini şişirir.
Böyle senaryolarda AsSplitQuery yaklaşımı, ilişki yüklemeyi birden fazla sorguya bölerek “kartesyen patlamayı” azaltabilir. Buradaki amaç N+1 üretmek değil; kontrolü sizde olan, daha öngörülebilir bir sorgu seti oluşturmaktır.
Include ile doğru kapsam
“Ne olur ne olmaz” diye tüm ilişkileri Include etmek, performansı iyileştirmek yerine bozabilir. Liste ekranında sadece isim ve toplam gerekiyorsa, ilişkili tablonun tüm kolonlarını getirmek maliyettir. Include’ları ekran ihtiyacına göre sınırlayın.

SplitQuery ne zaman tercih edilir?
Birden fazla koleksiyon navigasyonu aynı anda yükleniyorsa ve satır sayısı büyüyorsa split yaklaşımı çoğu zaman daha stabil çalışır. Ancak her ek sorgunun ağ gecikmesi yaratacağını unutmayın. Denge; veri tekrar maliyeti ile sorgu sayısı maliyeti arasındadır.
Sayfalama, Sıralama ve Doğru İndeks Stratejisi
Performansın çoğu “az veri çekmek” ile kazanılır. Sayfalama (pagination) uygulamak sadece UI için değil, sistem sağlığı için de kritiktir. Büyük tabloları sıralayıp sayfalarken doğru indeks yoksa, veritabanı gereksiz tarama yapar.
Özellikle “CreatedAt desc ile son kayıtlar” gibi yaygın senaryolarda, sıralama alanı ve filtre alanı birlikte düşünülmelidir. EF Core tarafında doğru sorgu yazsanız bile, veritabanı tarafında indeks yoksa sonuç değişmez.
Skip/Take ve stabil sıralama
Sayfalama yaparken stabil bir sıralama şarttır. Sadece CreatedAt ile sıralamak, aynı timestamp değerlerinde sayfalar arası kaymalara neden olabilir. İkinci bir anahtar (Id gibi) eklemek hem stabiliteyi hem de plan tutarlılığını artırır.
Keyset pagination ile daha iyi ölçeklenme
Yüksek sayfa numaralarında Skip maliyeti artar. Keyset pagination (son görülen anahtar üzerinden devam) daha ölçeklenebilir bir yaklaşımdır. “Son 50 kaydı getir” veya “şu Id’den küçük olanları getir” gibi kurgular, indeksle çok daha iyi çalışır.
Derleme, Önbellek ve Tanılama: İleri Seviye Dokunuşlar
Sorgularınız benzer kalıplarda tekrarlanıyorsa compiled query yaklaşımı bazı senaryolarda fayda sağlar. Burada amaç; sorgu ağacının tekrar tekrar işlenmesini azaltmaktır. Ancak bu, kötü sorguyu iyi yapmaz; önce tracking/projection/N+1 gibi temel problemleri temizlemek gerekir.
Tanılama tarafında SQL log’larını üretim ortamına taşımak her zaman mümkün olmayabilir. Bunun yerine örnekleme (sampling), yavaş sorgu eşikleri, metrikler ve APM izleri ile “hangi sorgu yavaş” sorusuna cevap verecek bir görünürlük oluşturun.
Pratik bir öğrenme rotası
Bu konuları sistematik ilerlemek isterseniz, gerçek projelerde sık karşılaşılan senaryoları adım adım ele alan içerikler daha hızlı yol aldırır. İsterseniz eğitim sayfasına göz atabilirsiniz: Entity Framework Core Eğitimi.

Özet: En Büyük Kazanç Nereden Gelir?
İyi bir performans iyileştirmesi, tek bir sihirli ayar değil; doğru alışkanlıkların toplamıdır. Okuma senaryosunda tracking’i kapatmak, ihtiyaç kadar veri taşımak için projection uygulamak ve N+1’i erken yakalayıp çözmek genellikle en büyük kazançları sağlar.
Son olarak şunu unutmayın: Performans, kullanıcı deneyimi kadar maliyet demektir. Daha az sorgu, daha az veri, daha az CPU ve bellek; daha stabil bir sistem ve daha öngörülebilir ölçeklenme anlamına gelir.


