EF CORE PERFORMANS NEDİR?
Geliştirici topluluğunda yıllardır tekrarlanan bir cümle var: "ORM yavaştır, performans gerektiren yerde raw SQL kullan." Cümle yarı doğrudur. EF Core dikkatsiz kullanıldığında raw ADO.NET'in 10-50 katı yavaş olabilir. Ama doğru kullanıldığında aradaki fark çoğu uygulamada ölçülebilir bile değildir. Yani sorun ORM kavramı değil; çoğu kez yazılan LINQ ifadesinin arka planda hangi SQL'e dönüştüğünün ve veritabanına kaç tur gittiğinin görünmez olmasıdır.
Bu yazı EF Core ile çalışıp API yanıt sürelerinde, batch işlerde ya da bellek tüketiminde sorun yaşayan geliştiriciler için pratik bir çerçeve sunar. Her yerde geçerli sihirli ayar yok; ama bilmen gereken birkaç kalıp var ve bu kalıplar genelde uygulamanın %80'lik darboğazını çözer.
Performans Tartışmasının Kökeni
EF Core, geliştiriciye nesne dünyasında bir koleksiyon sorguluyormuş hissi verir. context.Orders.Where(o => o.Total > 1000).ToList() yazarsın, sonuç gelir. Yazdığın LINQ ifadesinin arka planda hangi JOIN'lere, hangi parameter binding'e, kaç tur veritabanı çağrısına dönüştüğü doğrudan görünmez. Bu soyutlama hız kazandırır ama aynı zamanda gizler.
Üretimde fark eden geliştirici şaşırır: tek satır LINQ, profiler'da 800 ms boyunca veritabanına 1247 ayrı sorgu atmaktadır. Ortada bir bug yok — kod tipik bir lazy loading senaryosudur. ORM her şeyi yanlış yapmadı; sadece geliştiricinin niyetini yanlış okudu. Performansa odaklanmak demek, ORM'yi bırakmak değil — ORM'ye ne sorduğunu netleştirmektir.
N+1 Sorgu Sorunu
EF Core performans dertlerinin belki en yaygını N+1 problemidir. Senaryo basit: 50 sipariş listeliyorsun, her siparişin satırlarını da gösteriyorsun. Naive yaklaşım şudur:
var orders = context.Orders.ToList();
foreach (var o in orders)
{
Console.WriteLine(o.Items.Count);
}Yüzeyde temiz görünür. Arka planda 1 sorgu siparişleri çeker, sonra her sipariş için ayrı bir sorgu detayları çeker — toplam 51 sorgu. 500 siparişte 501 round trip. Çözümün üç pratik biçimi vardır.
Include ile eager loading: Bağlı koleksiyon önceden tek JOIN sorgusuyla yüklenir.
var orders = context.Orders
.Include(o => o.Items)
.ToList();Projection ile sadece gereken alanlar: Tam entity'yi yüklemeye gerek yoksa anonymous type veya DTO'ya projecte et — JOIN tek sorgu, dönen veri çok küçük.
var summary = context.Orders
.Select(o => new { o.Id, ItemCount = o.Items.Count })
.ToList();Split query: Bir-çok ilişkide tek JOIN şişirilmiş satırlar üretir (kartezyen büyüme). EF Core 5+ ile AsSplitQuery() bunu iki ayrı sorguya böler — bazı dağılımlarda toplam süre düşer.

Change Tracking ve AsNoTracking
EF Core, sorgulanan her entity'yi change tracker'a kaydeder — daha sonra SaveChanges çağrıldığında değişen alanları bulmak için bir snapshot tutar. Bu, güncelleme akışlarında çok pratiktir. Ama sadece okuma yapan endpoint'lerde gereksizdir ve hem CPU hem bellek harcar.
Read-only senaryolarda AsNoTracking kullanılır:
var products = context.Products
.AsNoTracking()
.Where(p => p.Stock > 0)
.ToList();Tek bir API endpoint'inde dahi tipik kazanç %20-40 arasıdır. Geniş entity'lerde — örneğin 30 kolonlu bir tablo — fark daha da büyüktür. Sorgu DbContext seviyesinde de default no-tracking yapılabilir; ama bu, yanlışlıkla update kaybına yol açabileceği için bilinçli tercih edilmelidir.
Bir kenar durum: aynı sorgu içinde birden fazla referans aynı entity'ye gidiyorsa (örneğin self-join'lerde) ve sen aynı C# nesnesini paylaşmasını istiyorsan AsNoTrackingWithIdentityResolution kullanılır — tracking olmadan kimlik çözümleme yapar.
Projection ile Ağırlık Azaltma
EF Core sorgusu ToList ile bitirildiğinde tüm kolonlar SELECT'e dahil olur. Tablon 25 kolonsa ve sen sadece 3'üne ihtiyaç duyuyorsan, network üzerinden 25 kolon taşınır, materialize edilir ve sonra atılır. Projection bu israfı keser:
// Kötü: tüm User kolonları çekilir
var emails = context.Users
.Where(u => u.Active)
.ToList()
.Select(u => u.Email);
// İyi: sadece email kolonu çekilir
var emails = context.Users
.Where(u => u.Active)
.Select(u => u.Email)
.ToList();İkinci versiyondaki Select, EF Core'a hangi kolonun ihtiyaç olduğunu söyler. Generated SQL artık SELECT Email FROM Users WHERE Active = 1 halindedir. Listeleme ve dashboard sorgularında projection'a alışmak tipik bir API'nin yanıt süresini yarıya indirebilir.
Asenkron Sorgular ve Throughput
Web API'lerde async/await sadece kod stilinin bir tercihi değil, throughput'un belirleyicisidir. Senkron ToList() çağrısı thread havuzundaki bir iş parçacığını veritabanı yanıtı gelene kadar bloke tutar. Aynı sunucu eş zamanlı 1000 istek aldığında havuz tükenir.
// Senkron: thread bloke kalır
var users = context.Users.Where(u => u.Active).ToList();
// Asenkron: I/O sırasında thread serbest
var users = await context.Users
.Where(u => u.Active)
.ToListAsync();Tek sayfalı bir isteğin süresi async ile dramatik kısalmaz — fark CPU'da değil, scheduler'da görünür. Ama yüksek concurrency altında async olan API, sync olana göre 3-5 kat daha fazla istek taşıyabilir. C# async/await mekaniğinin altına inmek isteyenler C# için detaylı eğitim içinde Task tabanlı eşzamanlılık konusunu bulabilir.

Compiled Queries ve Bulk İşlemler
Aynı sorgu farklı parametrelerle yüzlerce kez çalıştırılıyorsa, EF Core her seferinde LINQ ifadesini SQL'e çevirme maliyetini öder. Bu çeviri ucuz değildir. Compiled query ile çeviri bir kere yapılır, sonraki çağrılar bu önceden derlenmiş halini kullanır:
private static readonly Func<AppContext, int, Task<User>> GetUserById =
EF.CompileAsyncQuery((AppContext ctx, int id) =>
ctx.Users.FirstOrDefault(u => u.Id == id));
var user = await GetUserById(context, 42);Yüksek hit'li bir endpoint'te kazanç %10-30 bandında dolaşır. Çok yardımcı olduğu yer, dakikada binlerce çağrı alan tekil GET istekleridir.
Toplu güncelleme ve silme işlemlerinde ise EF Core 7 ile gelen ExecuteUpdate ve ExecuteDelete oyun değiştiricidir. Eskiden:
// Eski yaklaşım: tüm pasif kullanıcıları yükle, döngüde işaretle, kaydet
var inactive = context.Users.Where(u => !u.Active).ToList();
foreach (var u in inactive) u.Archived = true;
await context.SaveChangesAsync();10 bin kayıt için 10 bin UPDATE cümlesi anlamına gelir. Yeni yaklaşım:
await context.Users
.Where(u => !u.Active)
.ExecuteUpdateAsync(s => s.SetProperty(u => u.Archived, true));Tek bir UPDATE cümlesi veritabanında çalışır. Süre saniyelerden milisaniyelere iner. Aynı şekilde ExecuteDeleteAsync tek DELETE atar — change tracking, materialization yok.
Ölçmeden Optimize Etme
Performans çalışmalarının en kritik kuralı: optimize etmeden önce ölç. EF Core'da görünmeyeni görmek için iki araç vardır.
- Logging ile generated SQL:
optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information)diyerek tüm sorguları konsola düşürürsün. Hangi LINQ'in hangi SQL'e döndüğünü gözle görmek çoğu performans problemini ilk dakikada açığa çıkarır. - MiniProfiler: Web istekleri sırasında hangi sorgunun ne kadar sürdüğünü, kaç sorgu çağrıldığını yan panelde gösterir. Production-near ortamda gerçek davranışı yakalar.
- EFCore.AsyncQuery analyzer veya QueryTrackingBehavior loglama: Yanlışlıkla tracking ile dönen sorguları derleme veya çalışma anında işaretler.
Daha derin bir bakış için Microsoft'un yayımladığı EF Core performans dokümantasyonu hem teknik temelleri hem benchmark senaryolarını referans noktası olarak kullanılabilir biçimde işler.
Performans Optimizasyonunun Sıralaması
Sayısız yöntem var ama ROI farklı. Tipik bir EF Core projesinde uygulama sırası şudur:
- Önce N+1'i tespit et ve Include / projection ile çöz — genelde en büyük kazanç buradadır.
- Read-only endpoint'lere AsNoTracking ekle — risksiz, anında kazanç.
- Liste ve dashboard sorgularını projection'a taşı — payload boyutu ve hız aynı anda iyileşir.
- API'yi baştan sona async hale getir.
- Toplu işlemleri ExecuteUpdate / ExecuteDelete ile tek sorguya indir.
- Çok sık çağrılan ufak sorguları compiled query yap.
- Hâlâ darboğaz varsa raw SQL veya stored procedure — son çare.
Bu sırayı izleyen ekiplerde tipik bir API ortalama 800 ms yanıttan 120-150 ms'ye iner. Çoğu durumda raw SQL'e gerek bile kalmaz. EF Core ve diğer ORM'lerle çalışmayı yapılandırılmış biçimde öğrenmek isteyenler için Entity Framework Core eğitimi bu konuyu sıfırdan ileri seviyeye taşır; performans testi ve profiling tarafına yönelmek isteyenler .NET test ve performans eğitimi içinde benchmark ve load testing pratiklerini bulabilir.
Gözden Kaçan Sık Tuzaklar
Pratikte tekrarlayan, hızlıca düzeltilebilen birkaç hata noktası daha vardır:
- Client-side evaluation: EF Core 3+ versiyonlarında veritabanına çevrilemeyen LINQ ifadeleri çalışma zamanında exception atar. Eski projelerden gelenler bu davranışa hazırlıksız yakalanabilir.
- Indeksiz Where: EF Core sorgusu güzel görünür ama altta indeksi olmayan kolonda tarama yapıyorsa süre kolonu indekslemekle çözülür — kod tarafında değil.
- Long-lived DbContext: Bir DbContext'i request boyu yerine uygulama boyu kullanmak change tracker'ı dolu tutar; bellek sızdırır.
- SaveChanges döngü içinde: Her iterasyonda SaveChanges çağırmak transaction-per-row anlamına gelir. Toplu işlemlerde batch yapılır.
- Global query filter ihmal: Soft delete için global filter koyduysan ve sonra raw SQL yazıyorsan filter uygulanmaz; veri kaçar.
Performans çalışmasının kalıcı sırrı bu basit: ORM'yi şüpheyle değil ölçümle yargıla. EF Core arkasında ne olduğunu görüyor ve niyetini doğru anlatıyorsan, çoğu uygulamada ek bir katman yazmana gerek kalmaz.



