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

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

Ответ

Оптимизация запросов на чтение в EF Core — ключевой навык для повышения производительности приложения. Вот основные практики:

1. Отключение отслеживания изменений (AsNoTracking) Используйте AsNoTracking() для запросов, где данные только читаются и не будут изменяться. Это исключает накладные расходы на создание и хранение объектов ChangeTracker.

var products = await context.Products
    .AsNoTracking()
    .Where(p => p.Price > 100)
    .ToListAsync();

2. Точечная выборка данных (Проекции) Никогда не выбирайте всю сущность (SELECT *), если нужны лишь несколько полей. Используйте Select.

var productData = await context.Products
    .Where(p => p.IsActive)
    .Select(p => new { p.Id, p.Name, p.Price }) // Анонимный тип или DTO
    .ToListAsync();

3. Избирательная загрузка связанных данных Избегайте Include без необходимости. Используйте Select для явной загрузки нужных полей связанных сущностей, чтобы предотвратить проблему SELECT N+1 и избыточные JOIN.

// Плохо: может загрузить все поля Order и Customer
var orders = context.Orders.Include(o => o.Customer).ToList();

// Хорошо: загружаем только нужные данные
var orders = context.Orders
    .Select(o => new {
        OrderId = o.Id,
        CustomerName = o.Customer.Name,
        Total = o.TotalAmount
    }).ToList();

4. Пакетирование запросов EF Core 5+ группирует несколько операций SaveChanges в один вызов к БД. Для чтения используйте явное объединение логики в один запрос, а не несколько последовательных ToListAsync.

5. Использование индексов на уровне БД Убедитесь, что поля, используемые в условиях Where, OrderBy и Join, проиндексированы. Это решает проблему на уровне источника данных.

6. Кэширование Для статических или редко меняющихся данных (справочники) реализуйте кэширование (например, с помощью IMemoryCache или IDistributedCache), чтобы полностью избежать обращения к базе.

var categories = await memoryCache.GetOrCreateAsync("all_categories", async entry =>
{
    entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
    return await context.Categories.AsNoTracking().ToListAsync();
});

7. Анализ запросов Всегда проверяйте сгенерированный SQL через context.LogTo(Console.WriteLine) или профилировщик БД, чтобы убедиться в его эффективности.