ORTA
Loading Strategies (Veri Yükleme)
Bir entity'yi çektiğinde ilişkili verileri (navigation property'ler) ne zaman ve nasıl yükleyeceğini belirler. Yanlış tercih N+1 problemine yol açar — yüzlerce gereksiz sorgu.
Loading Stratejileri: Karar Ağacı
Eager Loading (Hevesli Yükleme)
Ana sorguyla birlikte ilişkili veriyi de çek. Include() kullanılır.
// 1️⃣ Tekil ilişki — Ürünleri kategorileriyle birlikte çek
var products = context.Products
.Include(p => p.Category)
.ToList();
-- 1️⃣ Include(p => p.Category) — tekil referans, her satır = 1 ürün
-- ORDER BY yok: Category reference navigation (1:1 eşleşme, gruplama gereksiz)
SELECT [p].[Id], [p].[Name], [p].[Price], [p].[CategoryId],
[c].[Id], [c].[Name]
FROM [Products] AS [p]
INNER JOIN [Categories] AS [c] ON [p].[CategoryId] = [c].[Id];
-- 1️⃣ Include(p => p.Category) — tekil referans, her satır = 1 ürün
-- ORDER BY yok: Category reference navigation (1:1 eşleşme, gruplama gereksiz)
SELECT p.id, p.name, p.price, p.category_id,
c.id, c.name
FROM products AS p
INNER JOIN categories AS c ON p.category_id = c.id;
// 2️⃣ İç içe ilişki (Nested Include) — Kategori → Ürünler → Taglar
var categories = context.Categories
.Include(c => c.Products)
.ThenInclude(p => p.Tags)
.ToList();
-- 2️⃣ Include + ThenInclude (iki seviye JOIN)
-- ORDER BY'ı EF Core otomatik ekler (entity materialization için gerekli)
SELECT [c].[Id], [c].[Name],
[p].[Id], [p].[Name], [p].[CategoryId],
[t].[Id], [t].[Name], [t].[ProductId]
FROM [Categories] AS [c]
LEFT JOIN [Products] AS [p] ON [p].[CategoryId] = [c].[Id]
LEFT JOIN [Tags] AS [t] ON [t].[ProductId] = [p].[Id]
ORDER BY [c].[Id], [p].[Id];
-- 2️⃣ Include + ThenInclude (iki seviye JOIN)
-- ORDER BY'ı EF Core otomatik ekler (entity materialization için gerekli)
SELECT c.id, c.name,
p.id, p.name, p.category_id,
t.id, t.name, t.product_id
FROM categories AS c
LEFT JOIN products AS p ON p.category_id = c.id
LEFT JOIN tags AS t ON t.product_id = p.id
ORDER BY c.id, p.id;
// 3️⃣ Filtrelenmiş Include (EF Core 5+) — sadece aktif ürünleri çek
var categories = context.Categories
.Include(c => c.Products.Where(p => p.IsActive))
.ToList();
-- 3️⃣ Filtered Include — WHERE koşulu JOIN'a eklenir
SELECT [c].[Id], [c].[Name],
[p].[Id], [p].[Name], [p].[CategoryId], [p].[IsActive]
FROM [Categories] AS [c]
LEFT JOIN [Products] AS [p] ON [p].[CategoryId] = [c].[Id] AND [p].[IsActive] = CAST(1 AS bit)
ORDER BY [c].[Id];
-- 3️⃣ Filtered Include — WHERE koşulu JOIN'a eklenir
SELECT c.id, c.name,
p.id, p.name, p.category_id, p.is_active
FROM categories AS c
LEFT JOIN products AS p ON p.category_id = c.id AND p.is_active = TRUE
ORDER BY c.id;
// 4️⃣ Birden fazla Include — Ürün + Kategori + Tedarikçi
var products = context.Products
.Include(p => p.Category)
.Include(p => p.Supplier)
.Where(p => p.Price > 100)
.ToList();
-- 4️⃣ Çoklu Include — her biri ayrı JOIN
SELECT [p].[Id], [p].[Name], [p].[Price], [p].[CategoryId], [p].[SupplierId],
[c].[Id], [c].[Name],
[s].[Id], [s].[CompanyName]
FROM [Products] AS [p]
INNER JOIN [Categories] AS [c] ON [p].[CategoryId] = [c].[Id]
INNER JOIN [Suppliers] AS [s] ON [p].[SupplierId] = [s].[Id]
WHERE [p].[Price] > 100.0;
-- 4️⃣ Çoklu Include — her biri ayrı JOIN
SELECT p.id, p.name, p.price, p.category_id, p.supplier_id,
c.id, c.name,
s.id, s.company_name
FROM products AS p
INNER JOIN categories AS c ON p.category_id = c.id
INNER JOIN suppliers AS s ON p.supplier_id = s.id
WHERE p.price > 100.0;
Explicit Loading (Açık Yükleme)
Sonradan, ihtiyaç duyulduğunda yükle. İki ayrı sorgu üretir.
// 1️⃣ Tekil navigation (Reference)
var product = context.Products.First(p => p.Id == 1);
context.Entry(product)
.Reference(p => p.Category)
.Load();
-- İlk sorgu:
SELECT TOP(1) [p].[Id], [p].[Name], [p].[CategoryId]
FROM [Products] AS [p]
WHERE [p].[Id] = 1;
-- .Load() çağrıldığında (ayrı sorgu):
SELECT [c].[Id], [c].[Name]
FROM [Categories] AS [c]
WHERE [c].[Id] = @categoryId; -- product.CategoryId değeri
-- İlk sorgu:
SELECT p.id, p.name, p.category_id
FROM products AS p
WHERE p.id = 1
LIMIT 1;
-- .Load() çağrıldığında (ayrı sorgu):
SELECT c.id, c.name
FROM categories AS c
WHERE c.id = @categoryId;
// 2️⃣ Koleksiyon navigation (Collection) — filtreleme ile
var category = context.Categories.First(c => c.Id == 1); // 1. sorgu: Category
context.Entry(category)
.Collection(c => c.Products)
.Query() // IQueryable döner — filtrelenebilir!
.Where(p => p.Price > 500)
.Load(); // 2. sorgu: filtrelenmiş Products
2 ayrı sorgu çalışır: İlk satır Category'yi,
.Load()ise Products'ı getirir. Bu Explicit Loading'in tasarımı gereğidir — avantajı ikinci sorguda.Where()ile filtreleme yapabilmenizdir (Eager Loading'de tüm ilişkili veri gelir).
-- .Query().Where(...).Load()
SELECT [p].[Id], [p].[Name], [p].[Price], [p].[CategoryId]
FROM [Products] AS [p]
WHERE [p].[CategoryId] = @categoryId AND [p].[Price] > 500.0;
-- .Query().Where(...).Load()
SELECT p.id, p.name, p.price, p.category_id
FROM products AS p
WHERE p.category_id = @categoryId AND p.price > 500.0;
Lazy Loading (Tembel Yükleme)
Navigation property'ye erişildiğinde otomatik yükler. N+1 sorgu tehlikesi — production'da dikkatli kullan!
// 1. NuGet: Microsoft.EntityFrameworkCore.Proxies
// 2. DbContext'te aktifleştir:
options.UseLazyLoadingProxies();
// 3. Navigation property'ler virtual olmalı:
public class Product
{
public int Id { get; set; }
public virtual Category Category { get; set; } // virtual!
}
// Kullanım — eriştiğinde otomatik SQL çalışır
var product = context.Products.First();
var categoryName = product.Category.Name; // ← Burada SELECT çalışır!
-- product.Category.Name erişildiğinde arka planda:
SELECT [c].[Id], [c].[Name]
FROM [Categories] AS [c]
WHERE [c].[Id] = @categoryId;
-- product.Category.Name erişildiğinde arka planda:
SELECT c.id, c.name
FROM categories AS c
WHERE c.id = @categoryId;
N+1 Problemi — Lazy Loading'in tehlikesi:
var products = context.Products.ToList(); // 1 sorgu foreach (var p in products) Console.WriteLine(p.Category.Name); // N sorgu! // Toplam: 1 + N sorgu (100 ürün = 101 sorgu!)Çözüm:
Include()kullan veya projection (Select) yap.
-- Döngüdeki her iterasyon için ayrı sorgu:
SELECT * FROM Products; -- 1. sorgu
SELECT * FROM Categories WHERE Id = 1; -- 2. sorgu
SELECT * FROM Categories WHERE Id = 2; -- 3. sorgu
SELECT * FROM Categories WHERE Id = 1; -- 4. (aynı bile olsa!)
-- ... 100 ürün = 101 sorgu!
-- Döngüdeki her iterasyon için ayrı sorgu:
SELECT * FROM products; -- 1. sorgu
SELECT * FROM categories WHERE id = 1; -- 2. sorgu
SELECT * FROM categories WHERE id = 2; -- 3. sorgu
SELECT * FROM categories WHERE id = 1; -- 4. (aynı bile olsa!)
-- ... 100 ürün = 101 sorgu!
Split Query (EF Core 5+)
Büyük include'larda kartezyen patlamayı önler.
var orders = context.Orders
.Include(o => o.LineItems)
.Include(o => o.Payments)
.AsSplitQuery() // Tek büyük JOIN yerine birden fazla sorgu
.ToList();
-- AsSplitQuery: ayrı ayrı sorgular
SELECT [o].* FROM [Orders] AS [o];
SELECT [l].* FROM [OrderLineItems] AS [l] WHERE [l].[OrderId] IN (...);
SELECT [p].* FROM [Payments] AS [p] WHERE [p].[OrderId] IN (...);
-- AsSplitQuery: ayrı ayrı sorgular
SELECT o.* FROM orders AS o;
SELECT l.* FROM order_line_items AS l WHERE l.order_id = ANY(@orderIds);
SELECT p.* FROM payments AS p WHERE p.order_id = ANY(@orderIds);
IN (...)parametre limiti: EF Core, ilk sorgudan dönen tüm PK değerleriniINlistesine koyar.
- SQL Server: max 2.100 parametre → ~2.100 order'dan fazlası varsa EF otomatik olarak geçici tablo veya alt sorgu kullanır (EF Core 8+)
- PostgreSQL: max 65.535 parametre — daha rahat ama yine de dikkat
- 1M kayıt gibi büyük veri setlerinde:
AsSplitQuery()yerine pagination (Skip/Take) +Includeveya Projection (Select) kullan- En güvenli yaklaşım: Filtresiz tüm tabloyu çekmek yerine
WHEREile daralt, sonraAsSplitQuery()uygula
Single Query vs Split Query — Trade-off:
| Kriter | Single Query (varsayılan) | Split Query |
|---|---|---|
| Ağ roundtrip | 1 | N (include sayısı kadar) |
| Kartezyen patlama | Olabilir (satır tekrarı) | Olmaz |
| Veri tutarlılığı | Tek transaction snapshot | Sorgular arası veri değişebilir |
| Küçük veri seti | Daha hızlı | Gereksiz overhead |
| Büyük/derin include | Bellek patlar | Çok daha verimli |
// Global olarak tüm sorguları split yapma (dikkatli kullan)
options.UseSqlServer(conn, o => o.UseQuerySplittingBehavior(
QuerySplittingBehavior.SplitQuery));
// Tek sorgu için split'i kapat
var data = context.Orders
.Include(o => o.Items)
.AsSingleQuery() // Global split açıksa, bunu tek sorgu yap
.ToList();
Hangi stratejiyi ne zaman kullan?
| Durum | Strateji |
|---|---|
| İlişkili veriyi kesinlikle kullanacaksın | Eager Loading (Include) |
| Belki kullanacaksın, koşula bağlı | Explicit Loading |
| Web API, DTO dönüyorsun | Projection (Select) — en iyi! |
| Büyük include kartezyen patlıyor | AsSplitQuery() |
| Kullanma | Lazy Loading (N+1 riski!) |
// ✅ En iyi yol: Projection (sadece ihtiyacın olan alanları çek)
var result = context.Products
.Where(p => p.IsActive)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
CategoryName = p.Category.Name // EF otomatik JOIN yapar
})
.ToList();
-- Projection: Include yazmana gerek yok, EF navigation'ı otomatik JOIN'a çevirir
SELECT [p].[Id], [p].[Name], [c].[Name] AS [CategoryName]
FROM [Products] AS [p]
INNER JOIN [Categories] AS [c] ON [p].[CategoryId] = [c].[Id]
WHERE [p].[IsActive] = CAST(1 AS bit);
-- ✅ Sadece 3 sütun çekilir (tüm tablo değil)
-- Projection: Include yazmana gerek yok, EF navigation'ı otomatik JOIN'a çevirir
SELECT p.id, p.name, c.name AS category_name
FROM products AS p
INNER JOIN categories AS c ON p.category_id = c.id
WHERE p.is_active = TRUE;
-- ✅ Sadece 3 sütun çekilir (tüm tablo değil)
JOIN üretimi için DB'de FK constraint olması şart mı?
Hayır. EF Core SQL üretirken sadece kendi modelindeki konfigürasyona bakar (navigation property + Fluent API). DB'deki fiziksel FK constraint sorgu üretimini etkilemez.
Senaryo JOIN üretilir mi? Açıklama DB'de FK + EF config Standart kullanım DB'de FK + EF config Legacy DB, FK'sız çalışır DB'de FK + EF config Navigation yoksa derlenmez İkisinde de yok p.Categorydiye bir property yokDB'deki FK constraint'in görevi: veri bütünlüğü (orphan kayıt engelleme).
EF'teki config'in görevi: sorgu üretimi (JOIN, Include, navigation).
İkisi birbirinden bağımsız çalışır.
N+1 Problemini Tespit Etme
N+1, production'da en sık karşılaşılan performans sorunudur. Tespit etmenin en kolay yolu EF Core'un log'unu açmak:
// Program.cs veya DbContext'te
optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);
// Sonra Output penceresinde SQL satırlarını say:
// 1 sorgu = normal
// 101 sorgu (1 ana + 100 tekrar eden) = N+1 problemi!
Araç önerisi: MiniProfiler — her sayfanın üstünde kaç SQL çalıştığını gösterir. Geliştirme sırasında paha biçilmez.