# 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. - Published: 2026-04-15 - Updated: 2026-04-15 - Author: SharpSkill - Tags: entity-framework-core, dotnet, ef-core, performance, best-practices - Reading time: 12 min --- 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` 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. ```csharp // ProductRepository.cs public async Task> 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: ```csharp // Program.cs builder.Services.AddDbContext(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. ```csharp // OrderRepository.cs public async Task 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ń. ```csharp // PromotionService.cs 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: ```csharp // CleanupService.cs public async Task PurgeExpiredSessionsAsync(CancellationToken ct) { await _context.Sessions .Where(s => s.ExpiresAt < DateTime.UtcNow) .ExecuteDeleteAsync(ct); } ``` ## 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. ```csharp // ReportService.cs public async Task> 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ń. ```csharp // AppDbContext.cs protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .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: ```csharp // InvoiceRepository.cs public async Task> 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. ```csharp // Program.cs builder.Services.AddDbContext(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ń. > **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ń. ```csharp // AppDbContext.cs - OnModelCreating modelBuilder.Entity(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: ```csharp // Program.cs (development only) builder.Services.AddDbContext(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. ```csharp // CompiledQueries.cs public static class UserQueries { // compiled once, reused on every call public static readonly Func> 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); ``` ## 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](/technologies/dotnet/interview-questions/ef-core-advanced) omawia te wzorce w kontekście rozmów kwalifikacyjnych, a przewodnik [Clean Architecture z .NET](/blog/dotnet/clean-architecture-dotnet-practical-guide) demonstruje strukturę warstw repozytorium i serwisów opakowujących te zapytania. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/dotnet/ef-core-performance-best-practices