← Назад к вопросам

Как в EF Core избежать проблемы N+1 запросов? Что такое Include и ThenInclude?

2.2 Middle🔥 111 комментариев
#Базы данных и SQL

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Избегание проблемы N+1 запросов в Entity Framework Core

Проблема N+1 запросов — это распространенная антипаттерн производительности в ORM, когда для загрузки связанных данных выполняется один основной запрос и N дополнительных запросов для каждой загруженной сущности. В EF Core эта проблема возникает при ленивой загрузке (Lazy Loading) или неправильном использовании явной загрузки.

Стратегии решения проблемы N+1

1. Жадная загрузка с Include/ThenInclude

// ПЛОХО: N+1 запросов
var authors = context.Authors.ToList();
foreach (var author in authors)
{
    var books = author.Books.ToList(); // Отдельный запрос для каждого автора!
}

// ХОРОШО: всего 1 запрос
var authorsWithBooks = context.Authors
    .Include(a => a.Books) // Жадная загрузка книг
    .ToList();

2. Проекции (Select) для частичной загрузки

// Эффективно: загружаем только нужные данные
var authorData = context.Authors
    .Select(a => new 
    {
        a.Id,
        a.Name,
        BookTitles = a.Books.Select(b => b.Title).ToList()
    })
    .ToList();

3. Использование Split Queries

// Разделение на несколько запросов (полезно при множественных Include)
var authors = context.Authors
    .Include(a => a.Books)
    .Include(a => a.Publisher)
    .AsSplitQuery() // Разделяет на несколько SQL-запросов
    .ToList();

4. Явная загрузка с Load

// Контролируемая загрузка связанных данных
var author = context.Authors.First();
context.Entry(author)
    .Collection(a => a.Books)
    .Query()
    .Where(b => b.IsPublished)
    .Load(); // Явная загрузка с фильтрацией

Методы Include и ThenInclude

Include() — основной метод для загрузки связанных данных

// Загрузка одной связанной коллекции
var orders = context.Orders
    .Include(o => o.OrderItems)
    .ToList();

// Загрузка одиночной связанной сущности
var products = context.Products
    .Include(p => p.Category)
    .ToList();

ThenInclude() — метод для загрузки цепочек связей

// Многоуровневая загрузка (цепочка связей)
var authors = context.Authors
    .Include(a => a.Books)          // Первый уровень: книги
        .ThenInclude(b => b.Reviews) // Второй уровень: отзывы к книгам
    .Include(a => a.Publisher)      // Другой путь загрузки
    .ToList();

Варианты использования ThenInclude:

// Загрузка через коллекции
var customers = context.Customers
    .Include(c => c.Orders)
        .ThenInclude(o => o.OrderDetails)
            .ThenInclude(od => od.Product)
    .ToList();

// Комбинирование разных путей
var products = context.Products
    .Include(p => p.Supplier)
    .Include(p => p.Category)
        .ThenInclude(c => c.ParentCategory)
    .ToList();

Продвинутые техники и лучшие практики

Фильтрация и сортировка внутри Include

// EF Core 5.0+: фильтрация и сортировка при загрузке
var blogs = context.Blogs
    .Include(b => b.Posts
        .Where(p => p.IsPublished)
        .OrderBy(p => p.CreatedDate)
        .Take(5))
    .ToList();

Автоматическое включение глобальных фильтров

// В DbContext.OnModelCreating
modelBuilder.Entity<Blog>()
    .HasMany(b => b.Posts)
    .WithOne()
    .HasForeignKey(p => p.BlogId)
    .OnDelete(DeleteBehavior.Cascade);

modelBuilder.Entity<Post>()
    .HasQueryFilter(p => !p.IsDeleted);

Мониторинг производительности

// Включение логирования для анализа запросов
optionsBuilder.UseSqlServer(connectionString)
    .LogTo(Console.WriteLine, LogLevel.Information)
    .EnableSensitiveDataLogging();

Критические рекомендации

  1. Всегда проверяйте генерируемый SQL через логирование или профилировщик
  2. Избегайте глубоких цепочек Include — загружайте только необходимые данные
  3. Используйте AsNoTracking() при операциях только для чтения:
    var authors = context.Authors
        .Include(a => a.Books)
        .AsNoTracking()
        .ToList();
    
  4. Ограничивайте количество загружаемых данных с помощью Take/Skip
  5. Рассмотрите использование чистых SQL-запросов для сложных сценариев

Пример комплексного решения

public async Task<List<AuthorDto>> GetAuthorsWithBooksAsync(int page, int pageSize)
{
    return await context.Authors
        .Include(a => a.Books
            .Where(b => b.PublishedYear >= 2020)
            .OrderByDescending(b => b.Rating)
            .Take(10))
        .ThenInclude(b => b.Publisher)
        .Include(a => a.ContactInfo)
        .AsNoTracking() // Для операций чтения
        .OrderBy(a => a.LastName)
        .Skip((page - 1) * pageSize)
        .Take(pageSize)
        .Select(a => new AuthorDto
        {
            Id = a.Id,
            FullName = $"{a.FirstName} {a.LastName}",
            BooksCount = a.Books.Count,
            TopBooks = a.Books.Select(b => b.Title).ToList()
        })
        .ToListAsync();
}

Правильное использование Include/ThenInclude и других стратегий загрузки данных — ключ к созданию производительных приложений на EF Core. Всегда анализируйте сгенерированные запросы и тестируйте производительность при работе с большими объемами данных.

Как в EF Core избежать проблемы N+1 запросов? Что такое Include и ThenInclude? | PrepBro