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 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 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.
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:
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.
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.
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ń.
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:
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.
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ń.
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:
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.
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:
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ń.
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ń.
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:
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.
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.
// 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:
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 projekcjiSelectw 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
ExecuteUpdateAsynciExecuteDeleteAsyncdla operacji masowych - Adoptowanie operatora
LeftJoinz EF Core 10 zastępuje rozwlekły łańcuchGroupJoin/SelectMany/DefaultIfEmpty - Konfigurowanie nazwanych filtrów zapytań pozwala zarządzać soft-delete i multi-tenancy niezależnie
- Ustawienie
EnableRetryOnFailurei 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
Udostępnij
Powiązane artykuły

Pytania rekrutacyjne C# i .NET: Kompletny przewodnik 2026
17 najczesciej zadawanych pytan rekrutacyjnych z C# i .NET. LINQ, async/await, Dependency Injection, Entity Framework Core i ASP.NET Core ze szczegolowymi odpowiedziami.

.NET 8: Budowanie API z ASP.NET Core
Kompletny przewodnik po tworzeniu profesjonalnego REST API z .NET 8 i ASP.NET Core. Kontrolery, Entity Framework Core, walidacja i najlepsze praktyki.

Rust: Podstawy dla Doswiadczonych Programistow w 2026
Szybka nauka Rust z wykorzystaniem dotychczasowej wiedzy. Ownership, borrowing, lifetimes i kluczowe wzorce wyjasnione dla programistow przechodzacych z C++, Javy lub Pythona.