Когда использовать AsNoTracking, compiled queries и split queries?
Сначала попробуйте ответить сами30 секунд
AsNoTracking отключает ChangeTracker для запросов только на чтение. Compiled queries устраняют накладные расходы на трансляцию LINQ для «горячих» запросов. Split queries разбивают многоуровневые Include на несколько SQL-запросов, устраняя декартов взрыв.
AsNoTracking
По умолчанию EF Core отслеживает все загруженные сущности через ChangeTracker. Это нужно для SaveChanges(), но при чтении без последующих изменений — лишний расход памяти и CPU на snapshot-сравнение.
Используйте AsNoTracking(), когда:
- Данные нужны только для чтения (GET-эндпоинты, отчёты, экспорт)
- Загружается большое количество записей
- Сущности не будут передаваться в
Update()илиAttach()
var products = await context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();
Глобально можно задать через UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) в DbContextOptions, затем выборочно включать трекинг через AsTracking().
AsNoTracking, Compiled Queries и Split Queries в EF Core
Эти три механизма решают разные проблемы производительности. Их правильное применение существенно снижает нагрузку на CPU и память.
Compiled Queries
Каждый раз при выполнении LINQ-запроса EF Core проходит через несколько шагов: парсинг дерева выражений, трансляция в SQL, кэширование плана. Скомпилированный запрос (EF.CompileAsyncQuery) фиксирует первые два шага, пропуская их при повторных вызовах.
Применять стоит для «горячих» запросов, которые вызываются тысячи раз в секунду с одинаковой структурой но разными параметрами.
// Определяется один раз — в статическом поле
private static readonly Func<AppDbContext, int, Task<Product?>> GetProductById =
EF.CompileAsyncQuery((AppDbContext ctx, int id) =>
ctx.Products.FirstOrDefault(p => p.Id == id));
// Использование:
var product = await GetProductById(context, 42);
Прирост заметен на нагруженных API: экономия 0.5–2 мс на каждом вызове при простых запросах.
Split Queries
При загрузке коллекций через Include() EF Core по умолчанию генерирует один JOIN-запрос. Это приводит к «декартову взрыву»: если у Order есть 10 Items и 5 Tags, результирующих строк будет 50. Split Queries разбивают одну операцию на несколько отдельных SQL-запросов.
var orders = await context.Orders
.Include(o => o.Items)
.Include(o => o.Tags)
.AsSplitQuery() // или UseSplitQuery() глобально
.ToListAsync();
Глобально:
optionsBuilder.UseSqlServer(connectionString, o =>
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
Split Queries эффективны при большом количестве Include-коллекций, но создают несколько round-trips к БД. При транзакционной важности согласованности — оставляйте единый запрос.
Подводные камни
AsNoTrackingломает lazy loading — если навигационные свойства не загружены явно, они будут null.- Compiled queries не поддерживают динамические условия (нельзя добавлять
Whereснаружи). - Split Queries не атомарны — между запросами другой процесс может изменить данные, вызвав рассогласование.
AsNoTrackingWithIdentityResolutionнужен, когда сущность встречается в нескольких местах Include и должна быть одним объектом, но трекинг не нужен.- Глобальное
NoTrackingпри использовании Identity Map (повторные запросы по Id) даст дублированные объекты в памяти. - Compiled queries кэшируются per-DbContext-type, не per-instance — нельзя использовать замыкания на переменные снаружи делегата.
- Split Queries увеличивают число соединений с БД, что критично при лимите пула соединений.
Типичные ошибки
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
Что проверяет интервьюер
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.