Entity Framework Core: оптимізація продуктивності та найкращі практики у 2026 році
Детальний посібник з оптимізації Entity Framework Core 10: налаштування запитів, пакетні операції, LeftJoin, іменовані фільтри та стратегії індексування.

Entity Framework Core 10, випущений разом із .NET 10 LTS у листопаді 2025 року, представляє оператори LeftJoin, векторний пошук, іменовані фільтри запитів та значні покращення трансляції SQL. Цей посібник охоплює найкращі практики EF Core, що безпосередньо впливають на швидкість запитів, розподіл пам'яті та масштабованість у продакшені.
EF Core 10 є релізом із довгостроковою підтримкою (LTS), який підтримуватиметься до листопада 2028 року. Він вимагає SDK та середовища виконання .NET 10. Застосунки, що досі працюють на .NET 8, повинні використовувати EF Core 8 (LTS) до завершення міграції.
Відстеження запитів: коли вимикати та чому
Кожен виклик DbSet<T> за замовчуванням приєднує повернені сутності до трекера змін. Трекер підтримує знімок оригінальних значень властивостей, обчислює різницю при виклику SaveChanges та розв'язує конфлікти ідентичності між навігаційними властивостями. Ці накладні витрати є зайвими, коли дані передаються безпосередньо до API-відповіді або моделі представлення лише для читання.
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 усуває витрати на створення знімків та розв'язання ідентичності для кожної сутності. У поєднанні з Select лише необхідні стовпці передаються через мережу. На таблиці з 50 000 рядків цей патерн зазвичай зменшує розподіл пам'яті на 40-60% порівняно з відстежуваним запитом повної сутності.
Для контекстів, що ніколи не модифікують дані, налаштування за замовчуванням встановлюється при реєстрації:
builder.Services.AddDbContext<CatalogContext>(options =>
options.UseSqlServer(connectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));Розділені запити для уникнення декартового вибуху
Завантаження сутності з кількома колекційними навігаційними властивостями через Include генерує один SQL-оператор із JOIN. Коли одночасно завантажуються дві або більше колекцій, результуючий набір зростає як декартовий добуток колекцій, дублюючи дані батьківського рядка для кожної комбінації.
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 розбиває завантаження на окремі SQL-оператори для кожної навігації. Компроміс: кілька обходів замість одного, але кожен результуючий набір залишається малим і уникає проблеми дублювання рядків. EF Core 10 також виправляє давню неузгодженість сортування у розділених запитах, забезпечуючи відповідність сортування підзапитів основному запиту.
Одиночні запити залишаються кращим вибором при завантаженні однієї колекційної навігації або коли затримка обходу висока (виклики бази даних між регіонами). Порівняйте продуктивність обох режимів для конкретного патерну доступу перед прийняттям рішення.
Пакетні операції з ExecuteUpdate та ExecuteDelete
Традиційні робочі процеси EF завантажують сутності, модифікують властивості, потім викликають SaveChanges. Для масових операцій, що впливають на тисячі рядків, це створює тисячі відстежуваних екземплярів та індивідуальних операторів UPDATE. EF Core 7 представив ExecuteUpdateAsync та ExecuteDeleteAsync для виконання операції одним SQL-командою. EF Core 10 спрощує API, приймаючи звичайну лямбду замість дерева виразів.
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
}Це транслюється в один оператор UPDATE ... SET ... WHERE. Жодна сутність не завантажується в пам'ять. Для оновлення 10 000 рядків час виконання скорочується з секунд (відстежуваний підхід) до мілісекунд.
Той самий патерн застосовується до видалень:
public async Task PurgeExpiredSessionsAsync(CancellationToken ct)
{
await _context.Sessions
.Where(s => s.ExpiresAt < DateTime.UtcNow)
.ExecuteDeleteAsync(ct);
}Готовий до співбесід з .NET?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Оператор LeftJoin в EF Core 10
До EF Core 10 виконання LEFT JOIN вимагало комбінування GroupJoin, SelectMany та DefaultIfEmpty у специфічному патерні, який більшість розробників мусили шукати щоразу. EF Core 10 додає LeftJoin як повноцінний 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);
}Створений SQL використовує стандартну клаузу LEFT JOIN. Оператор RightJoin також доступний. Обидва оператори усувають громіздкий ланцюжок із трьох методів, що раніше був необхідний.
Іменовані фільтри запитів для багатоцільової фільтрації
Глобальні фільтри запитів були обмежені одним фільтром на тип сутності з EF Core 2.0. Застосунки, що реалізують як м'яке видалення, так і багатоорендність, мусили поєднувати умови в один вираз і не могли вибірково їх вимикати. EF Core 10 представляє іменовані фільтри запитів.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Invoice>()
.HasQueryFilter("SoftDelete", i => !i.IsDeleted)
.HasQueryFilter("Tenant", i => i.TenantId == _tenantId);
}Адміністративні ендпоінти можуть вимикати фільтр м'якого видалення, зберігаючи активну ізоляцію орендарів:
public async Task<List<Invoice>> GetAllIncludingDeletedAsync(CancellationToken ct)
{
return await _context.Invoices
.IgnoreQueryFilters(["SoftDelete"]) // tenant filter still applied
.ToListAsync(ct);
}Це усуває потребу у власних методах розширення IQueryable, що вручну додавали умови фільтрації.
Стійкість з'єднання та конфігурація пулу
Тимчасові збої бази даних (мережеві перебої, переключення Azure SQL, вичерпання пулу з'єднань) спричиняють винятки, що часто призводять до збою конвеєрів запитів. EF Core надає вбудовану логіку повторних спроб, але значення за замовчуванням вимагають явної конфігурації.
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
}));Параметри пулу рядка підключення також мають значення:
Server=db.example.com;Database=AppDb;Min Pool Size=5;Max Pool Size=100;Connection Timeout=15;Min Pool Size зберігає підігріті з'єднання готовими до сплеску трафіку. Max Pool Size обмежує загальну кількість відкритих з'єднань для запобігання перевантаженню бази даних. Значення за замовчуванням 100 працює для більшості веб-застосунків, але високопропускні сервіси можуть потребувати налаштування на основі фактичного обсягу одночасних запитів.
Автоматичні повторні спроби не працюють всередині користувацьких транзакцій. Обгорніть всю транзакцію у ручну стратегію повторних спроб, використовуючи context.Database.CreateExecutionStrategy().ExecuteAsync(...) для обробки тимчасових збоїв у кількох операціях.
Стратегія індексування та аналіз запитів
Міграції EF Core можуть декларативно визначати індекси, але вибір стовпців для індексування вимагає розуміння патернів запитів.
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");
});Фільтровані індекси зменшують розмір індексу, виключаючи рядки, на які запити ніколи не націлюються. Для таблиці з 80% скасованих замовлень фільтрований індекс на активних статусах може бути в 5 разів меншим та швидшим для сканування.
Для аналізу згенерованого SQL увімкніть журналювання в середовищі розробки:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging());Скопіюйте записаний SQL у SQL Server Management Studio та виконайте його з SET STATISTICS IO ON для перевірки логічних читань, або використовуйте EXPLAIN ANALYZE на PostgreSQL. Пропозиції щодо відсутніх індексів із плану запиту часто виявляють можливості оптимізації з найбільшим впливом.
Скомпільовані запити для гарячих шляхів
Дерева виразів LINQ аналізуються та транслюються в SQL при кожному виконанні. Для запитів, що виконуються тисячі разів на хвилину (наприклад, перевірки автентифікації, валідація сесій), ці витрати на трансляцію накопичуються. Скомпільовані запити кешують трансляцію при запуску застосунку.
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);Скомпільовані запити повністю пропускають фазу аналізу дерева виразів. Різниця в продуктивності вимірювана лише на високочастотних шляхах (1000+ викликів/хвилину). Для стандартних CRUD-ендпоінтів вбудований кеш запитів у EF Core вже обробляє повторне використання трансляції.
Параметризована трансляція колекцій в EF Core 10
Запити з фільтрацією за списком ідентифікаторів представляють один із найпоширеніших патернів у доступі до даних. EF Core 10 змінює стратегію трансляції за замовчуванням для параметризованих колекцій. Замість кодування списку як JSON-масиву (EF Core 8-9) або вбудовування констант (EF Core 7 та раніше), EF Core 10 транслює кожне значення як окремий 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);Новий підхід надає планувальнику запитів бази даних інформацію про кардинальність, водночас уникаючи роздування кешу планів через доповнення параметрів. Для випадків, коли JSON-підхід працює краще (дуже великі колекції), поведінку можна перевизначити для конкретного запиту:
var orders = await _context.Orders
.Where(o => EF.Parameter(orderIds).Contains(o.Id)) // force JSON mode
.ToListAsync(ct);Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Висновок
- Використовуйте
AsNoTracking()та проєкціїSelectу кожному запиті лише для читання, щоб усунути накладні витрати трекера змін - Застосовуйте
AsSplitQuery()при завантаженні кількох колекційних навігацій для уникнення декартового вибуху - Замініть патерни відстежуваного завантаження-модифікації-збереження на
ExecuteUpdateAsyncтаExecuteDeleteAsyncдля масових операцій - Впроваджуйте оператор
LeftJoinз EF Core 10 для заміни громіздкого ланцюжкаGroupJoin/SelectMany/DefaultIfEmpty - Налаштуйте іменовані фільтри запитів для незалежного керування м'яким видаленням та багатоорендністю
- Встановіть
EnableRetryOnFailureта налаштуйте розміри пулу з'єднань для продакшн-стійкості - Визначайте композитні та фільтровані індекси на основі фактичних патернів запитів, а не припущень
- Резервуйте скомпільовані запити для справді гарячих шляхів, що перевищують 1000 виконань на хвилину
- Дозвольте EF Core 10 обробляти параметризовану трансляцію колекцій за замовчуванням і перевизначайте лише коли бенчмарки це виправдовують
Додаткове читання: модуль EF Core Advanced охоплює ці патерни в контексті співбесіди, а посібник Clean Architecture з .NET демонструє, як структурувати рівні репозиторіїв та сервісів, що обгортають ці запити.
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Теги
Поділитися
Пов'язані статті

Питання на співбесіді з C# та .NET: Повний посібник 2026
25 найпоширеніших питань на співбесіді з C# та .NET. LINQ, async/await, dependency injection, Entity Framework та найкращі практики з детальними відповідями.

.NET 8: Створення API з ASP.NET Core
Повний посібник зі створення професійного REST API з .NET 8 та ASP.NET Core. Контролери, Entity Framework Core, валідація та найкращі практики.

Rust: Основи для досвідчених розробників у 2026 році
Швидке освоєння Rust з використанням наявного досвіду програмування. Власність, запозичення, часи життя та ключові патерни для розробників, які переходять з C++, Java або Python.