Как оптимизировать запрос в Entity Framework Core?

«Как оптимизировать запрос в Entity Framework Core?» — вопрос из категории Entity Framework, который задают на 28% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Оптимизация запросов EF Core — это многоуровневый процесс. Вот ключевые стратегии:

1. Выбирайте только нужные данные (Projection) Избегайте SELECT *. Используйте Select() для загрузки конкретных полей или анонимных типов.

// ПЛОХО: Загружает все поля User
var users = await context.Users.ToListAsync();

// ХОРОШО: Загружает только Id и Name
var users = await context.Users
    .Select(u => new { u.Id, u.Name })
    .ToListAsync();

2. Отключайте отслеживание изменений для запросов только для чтения Метод AsNoTracking() значительно ускоряет выполнение, так как контекст не создает "снимки" сущностей.

var products = await context.Products
    .AsNoTracking() // Критически важно для read-only сценариев
    .Where(p => p.IsActive)
    .ToListAsync();

3. Эффективно загружайте связанные данные

  • Жадная загрузка (Include / ThenInclude): Используйте для данных, которые точно понадобятся. Будьте осторожны с цепочками, чтобы не получить "звездный" запрос.
  • Явная загрузка (Load): Для загрузки по требованию.
  • Выборочная загрузка (Projection с Include): Самый эффективный способ.
    // Эффективно: Загружает только имена заказов и клиентов
    var orders = await context.Orders
    .Where(o => o.Date.Year == 2023)
    .Select(o => new {
        o.Id,
        o.Total,
        CustomerName = o.Customer.Name // Загружается в том же запросе!
    })
    .ToListAsync();

4. Фильтруйте и пагинируйте данные на стороне БД Всегда используйте Where(), Take(), Skip() до вызова ToListAsync().

// Пагинация на стороне БД
var page = await context.Products
    .Where(p => p.CategoryId == 5)
    .OrderBy(p => p.Name)
    .Skip(20)
    .Take(10)
    .AsNoTracking()
    .ToListAsync();

5. Используйте индексы в базе данных EF Core может создавать их через миграции, но проектирование индексов — задача разработчика/администратора БД.

6. Анализируйте сгенерированный SQL

var query = context.Users.Where(u => u.IsActive);
var sql = query.ToQueryString(); // Получить SQL
// Или используйте логирование (context.LogTo) или профилировщик БД.

7. Избегайте проблемы N+1 Плохой паттерн (N+1 запрос):

var authors = context.Authors.ToList(); // 1 запрос
foreach (var a in authors)
{
    // N запросов (по одному на каждого автора)!
    var books = context.Books.Where(b => b.AuthorId == a.Id).ToList();
}

Исправление: Используйте Include или проекцию с Select для загрузки всех данных за один запрос.