Entity Framework Core: Optymalizacja wydajności i najlepsze praktyki w 2026

Kompleksowy przewodnik po optymalizacji EF Core 10. Poznaj techniki zwiększania wydajności zapytań, zarządzania pamięcią i skalowania aplikacji .NET.

Entity Framework Core performance optimization guide

Entity Framework Core 10, dostarczony wraz z .NET 10 LTS w listopadzie 2025, wprowadza operatory LeftJoin, wyszukiwanie wektorowe, nazwane filtry zapytań oraz znaczące usprawnienia translacji SQL. Niniejszy przewodnik przedstawia najlepsze praktyki EF Core, które bezpośrednio wpływają na szybkość zapytań, alokację pamięci i skalowalność w środowisku produkcyjnym.

EF Core 10 wymaga .NET 10

EF Core 10 to wydanie Long-Term Support, wspierane do listopada 2028. Wymaga SDK i runtime .NET 10. Aplikacje nadal działające na .NET 8 powinny używać EF Core 8 (LTS) do czasu zakończenia migracji.

Śledzenie zapytań: kiedy je wyłączyć i dlaczego

Każde wywołanie DbSet<T> domyślnie dołącza zwrócone encje do mechanizmu śledzenia zmian. Tracker utrzymuje migawkę oryginalnych wartości właściwości, oblicza różnice podczas SaveChanges i rozwiązuje konflikty tożsamości między nawigacjami. Ten narzut jest zbędny, gdy dane trafiają bezpośrednio do odpowiedzi API lub modelu widoku tylko do odczytu.

ProductRepository.cscsharp
public async Task<List<ProductDto>> GetActiveByCategoryAsync(
    int categoryId, CancellationToken ct)
{
    return await _context.Products
        .AsNoTracking()              // skip change tracker entirely
        .Where(p => p.CategoryId == categoryId && p.IsActive)
        .OrderBy(p => p.Name)
        .Select(p => new ProductDto   // project to DTO at the database level
        {
            Id = p.Id,
            Name = p.Name,
            Price = p.Price
        })
        .ToListAsync(ct);
}

AsNoTracking eliminuje narzut tworzenia migawek i rozwiązywania tożsamości dla każdej encji. W połączeniu z Select tylko niezbędne kolumny są przesyłane przez sieć. Na tabeli zawierającej 50 000 wierszy ten wzorzec zazwyczaj redukuje alokację pamięci o 40-60% w porównaniu do śledzonego zapytania pobierającego pełne encje.

Dla kontekstów, które nigdy nie modyfikują danych, warto ustawić domyślne zachowanie podczas rejestracji:

Program.cscsharp
builder.Services.AddDbContext<CatalogContext>(options =>
    options.UseSqlServer(connectionString)
           .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));

Zapytania podzielone przeciwko eksplozji kartezjańskiej

Ładowanie encji z wieloma nawigacjami kolekcji poprzez Include generuje pojedynczą instrukcję SQL z JOIN-ami. Gdy dwie lub więcej kolekcji jest ładowanych jednocześnie, zbiór wyników rośnie jako iloczyn kartezjański kolekcji, duplikując dane wiersza nadrzędnego dla każdej kombinacji.

OrderRepository.cscsharp
public async Task<Order?> GetWithDetailsAsync(int orderId, CancellationToken ct)
{
    return await _context.Orders
        .AsSplitQuery()              // one SQL per Include
        .Include(o => o.Items)
            .ThenInclude(i => i.Product)
        .Include(o => o.Payments)
        .Include(o => o.ShippingEvents)
        .FirstOrDefaultAsync(o => o.Id == orderId, ct);
}

AsSplitQuery dzieli ładowanie na oddzielne instrukcje SQL dla każdej nawigacji. Kompromis polega na wielokrotnych rundach komunikacji zamiast jednej, ale każdy zbiór wyników pozostaje mały i unika problemu duplikacji wierszy. EF Core 10 naprawia również długotrwałą niespójność sortowania w zapytaniach podzielonych, zapewniając zgodność sortowania podzapytań z zapytaniem głównym.

Kiedy używać pojedynczych zapytań

Pojedyncze zapytania pozostają preferowane przy ładowaniu jednej nawigacji kolekcji lub gdy opóźnienie round-trip jest wysokie (wywołania bazy danych między regionami). Przed podjęciem decyzji należy przeprowadzić benchmark obu trybów dla konkretnego wzorca dostępu.

Operacje wsadowe z ExecuteUpdate i ExecuteDelete

Tradycyjne przepływy pracy EF ładują encje, modyfikują właściwości, a następnie wywołują SaveChanges. Dla operacji masowych wpływających na tysiące wierszy tworzy to tysiące śledzonych instancji i indywidualnych instrukcji UPDATE. EF Core 7 wprowadził ExecuteUpdateAsync i ExecuteDeleteAsync, aby przekazać operację do pojedynczego polecenia SQL. EF Core 10 dodatkowo upraszcza API, akceptując zwykłą lambdę zamiast drzewa wyrażeń.

PromotionService.cscsharp
public async Task ApplySeasonalDiscountAsync(
    int categoryId, decimal discountPercent, CancellationToken ct)
{
    var affected = await _context.Products
        .Where(p => p.CategoryId == categoryId && p.IsActive)
        .ExecuteUpdateAsync(s =>
        {
            s.SetProperty(p => p.Price, p => p.Price * (1 - discountPercent / 100));
            s.SetProperty(p => p.LastModified, DateTime.UtcNow);
        }, ct);

    // affected = number of rows updated
}

To przekłada się na pojedynczą instrukcję UPDATE ... SET ... WHERE. Żadne encje nie są ładowane do pamięci. Dla aktualizacji 10 000 wierszy czas wykonania spada z sekund (podejście ze śledzeniem) do milisekund.

Ten sam wzorzec ma zastosowanie do usuwania:

CleanupService.cscsharp
public async Task PurgeExpiredSessionsAsync(CancellationToken ct)
{
    await _context.Sessions
        .Where(s => s.ExpiresAt < DateTime.UtcNow)
        .ExecuteDeleteAsync(ct);
}

Gotowy na rozmowy o .NET?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Operator LeftJoin w EF Core 10

Przed EF Core 10 wykonanie LEFT JOIN wymagało połączenia GroupJoin, SelectMany i DefaultIfEmpty w określonym wzorcu, który większość programistów musiała za każdym razem sprawdzać w dokumentacji. EF Core 10 dodaje LeftJoin jako pełnoprawny operator LINQ.

ReportService.cscsharp
public async Task<List<EmployeeReportDto>> GetEmployeeDepartmentReportAsync(
    CancellationToken ct)
{
    return await _context.Employees
        .LeftJoin(
            _context.Departments,
            employee => employee.DepartmentId,
            department => department.Id,
            (employee, department) => new EmployeeReportDto
            {
                FullName = employee.FirstName + " " + employee.LastName,
                Department = department.Name ?? "Unassigned",
                HiredAt = employee.HiredAt
            })
        .OrderBy(r => r.FullName)
        .ToListAsync(ct);
}

Wygenerowany SQL używa standardowej klauzuli LEFT JOIN. Dostępny jest również operator RightJoin. Oba operatory eliminują rozwlekły łańcuch trzech metod, który był wcześniej wymagany.

Nazwane filtry zapytań dla wieloaspektowego filtrowania

Globalne filtry zapytań były ograniczone do pojedynczego filtra na typ encji od EF Core 2.0. Aplikacje implementujące zarówno soft-delete, jak i multi-tenancy musiały łączyć warunki w jedno wyrażenie i nie mogły selektywnie ich wyłączać. EF Core 10 wprowadza nazwane filtry zapytań.

AppDbContext.cscsharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Invoice>()
        .HasQueryFilter("SoftDelete", i => !i.IsDeleted)
        .HasQueryFilter("Tenant", i => i.TenantId == _tenantId);
}

Endpointy administracyjne mogą wyłączyć filtr soft-delete, zachowując aktywną izolację najemców:

InvoiceRepository.cscsharp
public async Task<List<Invoice>> GetAllIncludingDeletedAsync(CancellationToken ct)
{
    return await _context.Invoices
        .IgnoreQueryFilters(["SoftDelete"])  // tenant filter still applied
        .ToListAsync(ct);
}

To eliminuje potrzebę tworzenia niestandardowych metod rozszerzających IQueryable, które ręcznie dodawały warunki filtrów.

Odporność połączeń i konfiguracja puli

Przejściowe awarie bazy danych (zakłócenia sieci, przełączenia awaryjne Azure SQL, wyczerpanie puli połączeń) powodują wyjątki, które często przerywają potoki żądań. EF Core zapewnia wbudowaną logikę ponawiania prób, ale domyślne ustawienia wymagają jawnej konfiguracji.

Program.cscsharp
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString, sqlOptions =>
    {
        sqlOptions.EnableRetryOnFailure(
            maxRetryCount: 5,
            maxRetryDelay: TimeSpan.FromSeconds(10),
            errorNumbersToAdd: null);    // retry on all transient errors
        sqlOptions.CommandTimeout(30);   // 30-second command timeout
    }));

Parametry puli w connection string również mają znaczenie:

text
Server=db.example.com;Database=AppDb;Min Pool Size=5;Max Pool Size=100;Connection Timeout=15;

Min Pool Size utrzymuje ciepłe połączenia gotowe na skoki ruchu. Max Pool Size ogranicza całkowitą liczbę otwartych połączeń, aby zapobiec przeciążeniu bazy danych. Domyślna wartość 100 sprawdza się dla większości aplikacji webowych, ale usługi o wysokiej przepustowości mogą wymagać dostrojenia na podstawie rzeczywistej liczby współbieżnych zapytań.

Logika ponawiania i transakcje

Automatyczne ponawianie nie działa wewnątrz transakcji inicjowanych przez użytkownika. Całą transakcję należy opakować w ręczną strategię ponawiania przy użyciu context.Database.CreateExecutionStrategy().ExecuteAsync(...), aby obsłużyć przejściowe awarie w wielu operacjach.

Strategia indeksowania i analiza zapytań

Migracje EF Core pozwalają definiować indeksy deklaratywnie, ale wybór kolumn do indeksowania wymaga zrozumienia wzorców zapytań.

AppDbContext.cs - OnModelCreatingcsharp
modelBuilder.Entity<Order>(entity =>
{
    // composite index for frequent query pattern
    entity.HasIndex(o => new { o.CustomerId, o.Status, o.CreatedAt })
          .HasDatabaseName("IX_Order_Customer_Status_Date");

    // filtered index for active orders only
    entity.HasIndex(o => o.Status)
          .HasFilter("[Status] <> 'Cancelled'")
          .HasDatabaseName("IX_Order_ActiveStatus");
});

Indeksy filtrowane zmniejszają rozmiar indeksu poprzez wykluczenie wierszy, których zapytania nigdy nie dotyczą. Dla tabeli z 80% anulowanych zamówień filtrowany indeks na aktywnych statusach może być 5 razy mniejszy i szybszy do skanowania.

Aby analizować generowany SQL, należy włączyć logowanie w środowisku deweloperskim:

Program.cs (development only)csharp
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString)
           .LogTo(Console.WriteLine, LogLevel.Information)
           .EnableSensitiveDataLogging());

Skopiowanie zalogowanego SQL do SQL Server Management Studio i uruchomienie go z SET STATISTICS IO ON pozwala sprawdzić logiczne odczyty, lub użycie EXPLAIN ANALYZE na PostgreSQL. Sugestie brakujących indeksów z planu zapytań często ujawniają możliwości optymalizacji o największym wpływie.

Skompilowane zapytania dla gorących ścieżek

Drzewa wyrażeń LINQ są parsowane i tłumaczone na SQL przy każdym wykonaniu. Dla zapytań uruchamianych tysiące razy na minutę (np. wyszukiwanie uwierzytelnienia, walidacja sesji) ten koszt translacji się kumuluje. Skompilowane zapytania buforują translację przy starcie aplikacji.

CompiledQueries.cscsharp
public static class UserQueries
{
    // compiled once, reused on every call
    public static readonly Func<AppDbContext, string, CancellationToken, Task<UserSession?>>
        GetActiveSession = EF.CompileAsyncQuery(
            (AppDbContext ctx, string token, CancellationToken ct) =>
                ctx.UserSessions
                    .AsNoTracking()
                    .FirstOrDefault(s => s.Token == token && s.ExpiresAt > DateTime.UtcNow));
}

// Usage in middleware
var session = await UserQueries.GetActiveSession(dbContext, bearerToken, ct);

Skompilowane zapytania całkowicie pomijają fazę parsowania drzewa wyrażeń. Różnica wydajności jest mierzalna tylko na ścieżkach o wysokiej częstotliwości (1000+ wywołań/minutę). Dla standardowych endpointów CRUD wbudowana pamięć podręczna zapytań w EF Core już obsługuje ponowne wykorzystanie translacji.

Translacja sparametryzowanych kolekcji w EF Core 10

Zapytania filtrujące po liście identyfikatorów reprezentują jeden z najczęstszych wzorców w dostępie do danych. EF Core 10 zmienia domyślną strategię translacji dla sparametryzowanych kolekcji. Zamiast kodowania listy jako tablicy JSON (EF Core 8-9) lub wstawiania stałych inline (EF Core 7 i wcześniejsze), EF Core 10 tłumaczy każdą wartość jako oddzielny parametr SQL.

csharp
// Before (EF Core 8-9): JSON array parameter
// @__ids_0='[1,2,3]'
// SELECT ... WHERE Id IN (SELECT value FROM OPENJSON(@__ids_0))

// EF Core 10: individual parameters with padding
// SELECT ... WHERE Id IN (@ids1, @ids2, @ids3)

var orderIds = new[] { 101, 205, 389 };
var orders = await _context.Orders
    .Where(o => orderIds.Contains(o.Id))
    .ToListAsync(ct);

Nowe podejście dostarcza planerowi zapytań bazy danych informacje o kardynalności, jednocześnie unikając rozdęcia pamięci podręcznej planów poprzez dopełnianie parametrów. W przypadkach, gdy podejście JSON działa lepiej (bardzo duże kolekcje), można nadpisać zachowanie dla konkretnego zapytania:

csharp
var orders = await _context.Orders
    .Where(o => EF.Parameter(orderIds).Contains(o.Id))  // force JSON mode
    .ToListAsync(ct);

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Podsumowanie

  • Używanie AsNoTracking() i projekcji Select w każdym zapytaniu tylko do odczytu eliminuje narzut mechanizmu śledzenia zmian
  • Stosowanie AsSplitQuery() przy ładowaniu wielu nawigacji kolekcji zapobiega eksplozji kartezjańskiej
  • Zastępowanie wzorców śledzenie-modyfikacja-zapis metodami ExecuteUpdateAsync i ExecuteDeleteAsync dla operacji masowych
  • Adoptowanie operatora LeftJoin z EF Core 10 zastępuje rozwlekły łańcuch GroupJoin/SelectMany/DefaultIfEmpty
  • Konfigurowanie nazwanych filtrów zapytań pozwala zarządzać soft-delete i multi-tenancy niezależnie
  • Ustawienie EnableRetryOnFailure i dostrojenie rozmiarów puli połączeń zwiększa odporność produkcyjną
  • Definiowanie indeksów złożonych i filtrowanych na podstawie rzeczywistych wzorców zapytań, nie domysłów
  • Rezerwowanie skompilowanych zapytań dla naprawdę gorących ścieżek przekraczających 1000 wykonań na minutę
  • Pozwolenie EF Core 10 na domyślną obsługę translacji sparametryzowanych kolekcji z nadpisywaniem tylko gdy benchmarki to uzasadniają

Dalsza lektura: moduł EF Core Advanced omawia te wzorce w kontekście rozmów kwalifikacyjnych, a przewodnik Clean Architecture z .NET demonstruje strukturę warstw repozytorium i serwisów opakowujących te zapytania.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

#entity-framework-core
#dotnet
#ef-core
#performance
#best-practices

Udostępnij

Powiązane artykuły