EFEF Core Handbook

İLERİ

Bulk & Batch Operations

Tek tek SaveChanges() çağırmak 1000 kayıtta 1000 ayrı SQL üretir. Toplu işlemler (ExecuteUpdate, ExecuteDelete, BulkExtensions) bunu tek veya birkaç SQL'e indirir.

Veritabanı sağlayıcısı Bu sayfadaki eşleşen örnekleri seçilen sağlayıcıya göre gösterir.

EF Core Yerleşik (Built-in)

// AddRange — toplu INSERT (tek SaveChanges'ta batch olarak gider)
var products = Enumerable.Range(1, 1000)
    .Select(i => new Product { Name = $"Product {i}", Price = i * 10 })
    .ToList();

context.Products.AddRange(products);
await context.SaveChangesAsync();
// EF Core otomatik batch yapar (varsayılan MaxBatchSize: 42 — SQL Server)
-- EF Core batched INSERT (tek roundtrip'te birden fazla INSERT):
INSERT INTO [Products] ([Name], [Price]) VALUES (N'Product 1', 10);
INSERT INTO [Products] ([Name], [Price]) VALUES (N'Product 2', 20);
-- ... (batch halinde gönderilir, tek roundtrip)
-- EF Core batched INSERT (tek roundtrip'te birden fazla INSERT):
INSERT INTO products (name, price) VALUES ('Product 1', 10), ('Product 2', 20);
-- PostgreSQL multi-row VALUES ile tek statement'ta batch yapar

ExecuteUpdate & ExecuteDelete (EF Core 7+)

// ✅ Toplu güncelleme — entity YÜKLENMEDEN direkt SQL
var affected = await context.Products
    .Where(p => p.CategoryId == 5 && !p.IsActive)
    .ExecuteUpdateAsync(s => s
        .SetProperty(p => p.IsActive, true)
        .SetProperty(p => p.UpdatedAt, DateTime.UtcNow));

// ✅ Toplu silme
var deleted = await context.Products
    .Where(p => p.IsDeleted && p.DeletedAt < DateTime.UtcNow.AddYears(-1))
    .ExecuteDeleteAsync();

// ✅ Birden fazla property güncelleme
await context.Products
    .Where(p => p.Price < 100)
    .ExecuteUpdateAsync(s => s
        .SetProperty(p => p.Price, p => p.Price * 1.10m)    // %10 zam
        .SetProperty(p => p.UpdatedAt, DateTime.UtcNow));
-- ExecuteUpdate: tek SQL, Change Tracker bypass
UPDATE [Products]
SET [IsActive] = CAST(1 AS bit), [UpdatedAt] = '2025-03-15T14:30:00Z'
WHERE [CategoryId] = 5 AND [IsActive] = 0;

-- ExecuteDelete
DELETE FROM [Products]
WHERE [IsDeleted] = 1 AND [DeletedAt] < '2024-03-15T14:30:00Z';

-- %10 zam
UPDATE [Products]
SET [Price] = [Price] * 1.10, [UpdatedAt] = '2025-03-15T14:30:00Z'
WHERE [Price] < 100.0;
-- ExecuteUpdate: tek SQL, Change Tracker bypass
UPDATE products
SET is_active = TRUE, updated_at = '2025-03-15T14:30:00Z'
WHERE category_id = 5 AND is_active = FALSE;

-- ExecuteDelete
DELETE FROM products
WHERE is_deleted = TRUE AND deleted_at < '2024-03-15T14:30:00Z';

-- %10 zam
UPDATE products
SET price = price * 1.10, updated_at = '2025-03-15T14:30:00Z'
WHERE price < 100.0;

ExecuteUpdateAsync — Dinamik Lambda 🆕 EF Core 10 (GA)

EF Core 10 öncesinde ExecuteUpdateAsync bir expression tree kabul ediyordu — dinamik property güncellemesi çok zordu. Artık normal lambda destekleniyor:

// 🆕 EF Core 10 (GA): Normal lambda — koşullu property güncelleme
await context.Products.ExecuteUpdateAsync(s =>
{
    s.SetProperty(p => p.Views, 0);  // Her zaman sıfırla

    if (priceChanged)
    {
        s.SetProperty(p => p.Price, newPrice);      // Koşullu
    }

    if (nameChanged)
    {
        s.SetProperty(p => p.Name, newName);        // Koşullu
    }
});

// ❌ EF Core 9 ve öncesi: Aynı işi yapmak için Expression API gerekiyordu (20+ satır karmaşık kod)

Batch Size Ayarı

// MaxBatchSize ayarı — SQL Server varsayılan: 42
options.UseSqlServer(conn, o => o.MaxBatchSize(100));

// PostgreSQL varsayılan: 40 (Npgsql)
options.UseNpgsql(conn, o => o.MaxBatchSize(200));

3rd Party: EFCore.BulkExtensions

// NuGet: EFCore.BulkExtensions
using EFCore.BulkExtensions;

// Bulk Insert — SqlBulkCopy kullanır (çok hızlı)
await context.BulkInsertAsync(products);

// Bulk Update
await context.BulkUpdateAsync(products);

// Bulk Upsert (varsa güncelle, yoksa ekle)
await context.BulkInsertOrUpdateAsync(products);

// Bulk Delete
await context.BulkDeleteAsync(products);

Performans Karşılaştırma

Yöntem 10.000 kayıt INSERT 10.000 kayıt UPDATE
foreach + SaveChanges her seferinde ~30 sn ~30 sn
AddRange + SaveChanges (batched) ~3 sn
ExecuteUpdateAsync ~100 ms
BulkInsertAsync (3rd party) ~200 ms ~300 ms

ExecuteUpdate/Delete kısıtlamaları:

  • Change Tracker'ı bypass eder — tracked entity'ler güncel kalmaz
  • Interceptor / SaveChanges override çalışmaz (audit, soft delete tetiklenmez)
  • Navigation/Include kullanılamaz (sadece tek tablo)
  • Sonrasında context.ChangeTracker.Clear() çağırmak iyi pratiktir

Bulk/Batch İşlem Limitleri:

Limit PostgreSQL SQL Server Açıklama
Max parametre/sorgu 65.535 2.100 EF otomatik batch'ler
Max INSERT/batch (EF varsayılan) 42 (Npgsql) 42 MaxBatchSize ile değiştir
Max transaction süresi (pratik) < 5 dakika < 5 dakika Lock escalation, WAL şişmesi
PostgreSQL Bulk Insert — COPY Komutu

Toplu insert performans rehberi (PostgreSQL):

Kayıt Sayısı Önerilen Yöntem Neden
< 100 AddRange + SaveChanges EF batching yeterli
100 - 10.000 SaveChanges + MaxBatchSize=100 Parametre limiti içinde
10.000 - 1M COPY komutu (Npgsql) Binary protocol, en hızlı
> 1M COPY + devre dışı index + transaction Index rebuild sonradan
Limit Değer
COPY satır limiti Sınır yok (PG bulk import en hızlı yol)
Max WAL boyutu (checkpoint arası) max_wal_size (1 GB varsayılan)
// PostgreSQL COPY — en hızlı bulk insert yöntemi:
using var writer = conn.BeginBinaryImport(
    "COPY products (name, price, created_at) FROM STDIN (FORMAT BINARY)");

foreach (var product in products)
{
    writer.StartRow();
    writer.Write(product.Name, NpgsqlDbType.Text);
    writer.Write(product.Price, NpgsqlDbType.Numeric);
    writer.Write(product.CreatedAt, NpgsqlDbType.TimestampTz);
}

await writer.CompleteAsync();
// 1M kayıt: ~3-5 saniye (SaveChanges ile 10+ dakika olurdu)

Long-running transaction uyarısı:

  • PostgreSQL'de uzun transaction WAL dosyalarını şişirir (disk dolabilir)
  • VACUUM çalışamaz → dead tuple birikir → tablo şişer (table bloat)
  • Kural: Batch işlemleri 500-5000 kayıt bloklarına böl, her blokta COMMIT yap