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

Ответ

Оптимизация запросов на чтение в 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) или профилировщик БД, чтобы убедиться в его эффективности.

Ответ 18+ 🔞

Ну ты смотри, какие советы умные понаписали. А ведь реально, если этим не пользоваться, то твоё приложение будет жрать ресурсы как не в себя, а потом все будут удивляться — почему оно тормозит, как черепаха в патоках. Давай по-человечьи разберём.

1. Выключи слежку, когда не надо (AsNoTracking) Представь, EF Core — это такой дотошный надзиратель. Он за каждым объектом, который ты из базы вытащил, начинает следить: а не поменял ли ты чего? А это, на минуточку, память и процессорные циклы жрёт. Если ты просто читаешь данные, чтобы показать их на сайте, и не собираешься их тут же обновлять — скажи этому надзирателю «отъебись». Метод AsNoTracking() именно для этого и создан.

var products = await context.Products
    .AsNoTracking() // Вот эта волшебная строчка. Сказал и забыл.
    .Where(p => p.Price > 100)
    .ToListAsync();

После этого контекст про эти объекты сразу забывает, и жить становится легче.

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

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

Базе проще, сети меньше грузишь, память экономишь. Всё в плюсе.

3. Связанные данные — не панацея, а граната без чеки Метод Include() — это как мощный инструмент. В неумелых руках он устроит тебе SELECT N+1 или нагенерирует такие монструозные JOIN-ы, что база захлебнётся. Ты думаешь: «О, загружу заказы с клиентами, всё будет быстро». А на деле получается дикая порнография с кучей лишних полей.

// Пиздец как плохо: вытащит ВСЕ поля из Orders и ВСЕ поля из Customers. Зачем?
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();

Суть в том, чтобы руками, через Select, указать, что именно тебе нужно. Это даёт полный контроль.

4. Объединяй запросы, где можно EF Core 5 и выше умеет сам пачковать запросы на изменение. Но с чтением — твоя головная боль. Не делай десять мелких запросов в цикле. Собери всё в один нормальный, объёмный, но ЕДИНЫЙ запрос. Один вылет в базу всегда быстрее, чем двадцать.

5. Индексы в базе — это твои лучшие друзья Можно сколько угодно оптимизировать код, но если в базе на поле Price нет индекса, а ты по нему постоянно ищешь (Where), то запрос будет ползти, как рак по шоссе. Открой свою базу, посмотри на планы запросов и настрой индексы. Это не магия, это необходимость.

6. Кэшируй то, что не меняется Зачем каждый раз ходить в базу за списком категорий товаров или за странами мира, если они обновляются раз в год? Положи это дело в кэш, хоть в памяти, хоть в Redis.

var categories = await memoryCache.GetOrCreateAsync("all_categories", async entry =>
{
    entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); // Кэш на час, например.
    return await context.Categories.AsNoTracking().ToListAsync(); // Первый раз сходили в базу.
});

Все последующие запросы в течение часа будут брать данные из памяти, а база отдыхает. Производительность взлетает до небес.

7. Смотри, что EF Core творит под капотом Самая главная мысль. Ты должен ВИДЕТЬ SQL-запросы, которые генерирует твой код. Включи логирование: context.LogTo(Console.WriteLine). Увидишь какую-нибудь хуйню вроде 25 вложенных SELECT-ов — сразу поймёшь, где косяк. Без этого ты просто слепой, который на ощупь пытается починить реактивный двигатель.

Вот и вся философия. Ничего сверхъестественного, просто не ленись думать головой и проверять, что получается. А то напишешь кривой запрос, а потом вся команда месяц дебажит, почему сервер падает под нагрузкой.