Entity FrameworkSeniorSystem design
Как оптимизировать производительность EF Core для сценариев с интенсивным чтением?
Основные техники: AsNoTracking для всех read-only запросов, проекция в DTO через Select, Compiled Queries для горячих путей, Split Queries при множественных Include, keyset pagination вместо OFFSET и индексы через HasIndex. Для аналитики — Dapper или сырой SQL.
Оптимизация производительности EF Core для чтения
Сценарии с интенсивным чтением требуют системного подхода: от конфигурации контекста до правильного маппинга результатов. Ниже — конкретные техники с примерами.
1. AsNoTracking и AsNoTrackingWithIdentityResolution
// Простые запросы без навигационных свойств:
var products = await context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();
// Когда одна сущность может встретиться в нескольких Include:
var orders = await context.Orders
.AsNoTrackingWithIdentityResolution()
.Include(o => o.Items)
.ToListAsync();
2. Проекция в DTO вместо загрузки полных сущностей
Загружайте только нужные поля — SQL-запрос будет меньше, сеть и память используются эффективнее:
var dtos = await context.Products
.Where(p => p.CategoryId == categoryId)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync();
3. Compiled Queries для «горячих» путей
private static readonly Func<AppDbContext, int, IAsyncEnumerable<ProductDto>> GetByCategory =
EF.CompileAsyncQuery((AppDbContext ctx, int catId) =>
ctx.Products
.Where(p => p.CategoryId == catId)
.Select(p => new ProductDto { Id = p.Id, Name = p.Name }));
// Использование:
await foreach (var dto in GetByCategory(context, 5)) { ... }
4. Split Queries против декартова взрыва
var orders = await context.Orders
.Include(o => o.Items)
.Include(o => o.Tags)
.AsSplitQuery()
.ToListAsync();
5. Пагинация через OFFSET/FETCH или keyset
// Offset-based (медленно на больших таблицах):
var page = await context.Products
.OrderBy(p => p.Id)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.AsNoTracking()
.ToListAsync();
// Keyset pagination (быстро):
var page = await context.Products
.Where(p => p.Id > lastSeenId)
.OrderBy(p => p.Id)
.Take(pageSize)
.AsNoTracking()
.ToListAsync();
6. Индексы через конфигурацию модели
modelBuilder.Entity<Product>()
.HasIndex(p => new { p.CategoryId, p.IsActive })
.HasDatabaseName("IX_Products_Category_Active");
7. Глобальное отключение трекинга для read-only контекста
services.AddDbContextFactory<ReadOnlyDbContext>(options =>
options
.UseSqlServer(connectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
8. Использование ExecuteReader / Dapper для отчётов
Для сложных аналитических запросов EF Core может быть медленнее, чем прямой ADO.NET или Dapper. В таких сценариях комбинирование — нормальная практика.
Подводные камни
- AsNoTracking ломает lazy loading — загружайте всё через Include или явные запросы.
- Select с анонимными типами не работает с Compiled Queries — используйте именованные DTO.
- Split Queries создают несколько round-trips — при высокой latency к БД это может быть медленнее единого JOIN.
- OFFSET на больших таблицах (>100k строк) деградирует — применяйте keyset pagination.
- Computed columns, не включённые в индексы, часто приводят к Full Table Scan — проверяйте планы через
context.Database.ExecuteSqlRaw("EXPLAIN ..."). - Неправильно настроенный пул соединений ограничивает параллелизм при интенсивном чтении — настраивайте
MaxPoolSize. - Горячий путь через
ToListAsync()загружает всё в память — для больших наборов используйтеAsAsyncEnumerable()и обрабатывайте построчно.
Common mistakes
- Путать read-heavy оптимизация запросов EF Core с похожим механизмом из другой версии или платформы.
- Игнорировать runtime-границы Entity Framework: lifecycle, DI scope, SQL translation, UI thread или platform API.
- Не обсуждать null/empty/error cases и поведение под нагрузкой.
What the interviewer is testing
- Кандидат объясняет read-heavy оптимизация запросов EF Core на конкретном примере, а не только определением.
- Указывает последствия для производительности, тестируемости и поддержки.
- Различает документированное поведение текущего стека и устаревшие практики.