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 на конкретном примере, а не только определением.
  • Указывает последствия для производительности, тестируемости и поддержки.
  • Различает документированное поведение текущего стека и устаревшие практики.

Sources

Related topics