0Pricing
C# Academy · Урок

Компилируемые запросы и производительность

Повышайте производительность запросов с помощью EF.CompileQuery, AsNoTracking и проецирования через Select.

«Компилируемые запросы и производительность» — бесплатный урок C# Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.

Компиляция запросов EF Core

Каждый раз при выполнении запроса LINQ EF Core должен разобрать дерево выражения, перевести его в SQL и проверить. Для часто выполняемых участков эти накладные расходы суммируются. EF Core предоставляет несколько средств для их сокращения.

AsNoTracking для запросов только для чтения

AsNoTracking() пропускает регистрацию в трекере изменений. Для запросов только для чтения (ответов API, отчётов) это самый простой и значительный способ ускорения — обычно в 2–5 раз быстрее и с меньшим потреблением памяти.

// With tracking (default) — for entities you'll modify
var tracked = await _db.Products.FirstOrDefaultAsync(p => p.Id == 1);

// Without tracking — for read-only
var dto = await _db.Products
    .AsNoTracking()
    .Where(p => p.IsActive)
    .Select(p => new ProductDto { Id = p.Id, Name = p.Name })
    .ToListAsync();

Скомпилированные запросы

EF.CompileAsyncQuery заранее компилирует запрос LINQ в делегат. Перевод выполняется один раз при запуске, а последующие вызовы полностью пропускают этап компиляции.

// Define once, reuse many times
private static readonly Func<AppDbContext, int, Task<Product?>> GetProductById =
    EF.CompileAsyncQuery(
        (AppDbContext db, int id) =>
            db.Products.AsNoTracking().FirstOrDefault(p => p.Id == id));

// Usage
var product = await GetProductById(_db, 42);

Проекция с Select для повышения производительности

Всегда создавайте проекцию ровно нужных полей. Выбор DTO вместо полной сущности уменьшает количество загружаемых столбцов, объём данных в сети и нагрузку на сборщик мусора.

// Load entire entity (over-fetch)
var all = await _db.Products.ToListAsync();

// Projection: only fetch needed columns
var names = await _db.Products
    .AsNoTracking()
    .Select(p => new { p.Id, p.Name, p.Price })
    .ToListAsync();
// SQL: SELECT Id, Name, Price FROM Products

Подсказки индексов и оптимизация сырых запросов

EF Core учитывает индексы базы данных, настроенные с помощью HasIndex(). Составные и отфильтрованные индексы, а также включённые столбцы влияют на производительность плана запроса.

// Configure compound index in OnModelCreating
modelBuilder.Entity<Order>()
    .HasIndex(o => new { o.CustomerId, o.Status })
    .HasFilter("[Status] = 0")  // filtered index
    .IncludeProperties(o => o.PlacedAt);

// The query below will use this index efficiently
var pending = await _db.Orders
    .AsNoTracking()
    .Where(o => o.CustomerId == 5 && o.Status == OrderStatus.Pending)
    .ToListAsync();

Разбиение на страницы с помощью Skip и Take

Всегда разбивайте большие наборы результатов на страницы. Skip и Take преобразуются в SQL OFFSET/FETCH и не позволяют загружать в память тысячи строк.

int page = 2, pageSize = 25;

var products = await _db.Products
    .AsNoTracking()
    .Where(p => p.IsActive)
    .OrderBy(p => p.Name)
    .Skip((page - 1) * pageSize)
    .Take(pageSize)
    .ToListAsync();
// SQL: ... ORDER BY Name OFFSET 25 ROWS FETCH NEXT 25 ROWS ONLY

Эффективный подсчёт

Используйте CountAsync() или AnyAsync() вместо загрузки записей только ради их подсчёта. AnyAsync() ещё быстрее, чем COUNT, если нужно лишь проверить существование.

// Total count for pagination
int totalCount = await _db.Products
    .Where(p => p.IsActive)
    .CountAsync();

// Existence check — faster than Count > 0
bool hasExpensive = await _db.Products
    .AnyAsync(p => p.Price > 1000m);
// SQL: IF EXISTS (SELECT 1 FROM ...) SELECT 1

Массовая вставка с AddRange

Используйте AddRange, чтобы добавить сразу несколько сущностей. EF Core объединяет инструкции INSERT в меньшее число обращений к базе данных; размер пакета задаётся поставщиком через MaxBatchSize.

var newProducts = Enumerable.Range(1, 1000)
    .Select(i => new Product { Name = $"Product {i}", Price = i * 1.5m })
    .ToList();

_db.Products.AddRange(newProducts);
await _db.SaveChangesAsync();
// Batched: typically 100-1000 rows per round-trip

EF.Functions для функций базы данных

EF.Functions предоставляет функции конкретной базы данных (LIKE, части даты, полнотекстовый поиск), которые EF Core переводит в SQL, а не вычисляет в памяти.

// LIKE pattern search (SQL-side)
var results = await _db.Products
    .Where(p => EF.Functions.Like(p.Name, "%keyboard%"))
    .AsNoTracking()
    .ToListAsync();

// Date difference (SQL-side)
var recentOrders = await _db.Orders
    .Where(o => EF.Functions.DateDiffDay(o.PlacedAt, DateTime.UtcNow) <= 7)
    .ToListAsync();

Диагностика медленных запросов

Включите журналирование запросов EF Core, чтобы видеть сгенерированный SQL. В среде разработки используйте EnableSensitiveDataLogging(), чтобы видеть значения параметров.

builder.Services.AddDbContext<AppDbContext>(opt =>
    opt.UseSqlServer(connStr)
       .LogTo(Console.WriteLine, LogLevel.Information)
       .EnableSensitiveDataLogging() // dev only!
       .EnableDetailedErrors());     // dev only!

Реальный пример: оптимизированная конечная точка списка

Готовая для рабочего использования конечная точка списка сочетает AsNoTracking, проекцию, индексы и разбиение на страницы для максимальной производительности.

app.MapGet("/products", async (
    AppDbContext db,
    string? search,
    int page = 1, int size = 20) =>
{
    var query = db.Products
        .AsNoTracking()
        .Where(p => p.IsActive);

    if (!string.IsNullOrEmpty(search))
        query = query.Where(p => EF.Functions.Like(p.Name, $"%{search}%"));

    var total = await query.CountAsync();
    var items = await query
        .OrderBy(p => p.Name)
        .Skip((page - 1) * size).Take(size)
        .Select(p => new { p.Id, p.Name, p.Price })
        .ToListAsync();

    return Results.Ok(new { total, items });
});

Быстрая проверка

Каково главное преимущество EF.CompileAsyncQuery перед обычным запросом LINQ?

Итоги: скомпилированные запросы и производительность

Главное:

  • AsNoTracking(): пропускает отслеживание изменений для запросов только для чтения — самый простой способ ускорения
  • EF.CompileAsyncQuery: заранее компилирует часто выполняемые запросы в делегаты
  • Проекция с помощью Select: загружает только необходимые столбцы
  • Правильные индексы (HasIndex) значительно улучшают планы запросов
  • Разбивайте результаты на страницы с помощью Skip/Take; используйте AnyAsync для проверки существования
  • Включайте журналирование запросов в среде разработки, чтобы обнаруживать медленный или неожиданный SQL

Часто задаваемые вопросы

Урок «Компилируемые запросы и производительность» бесплатный?

Да — полный текст урока «Компилируемые запросы и производительность» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.

Чему я научусь в уроке «Компилируемые запросы и производительность»?

Повышайте производительность запросов с помощью EF.CompileQuery, AsNoTracking и проецирования через Select. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать C# Academy?

Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «Компилируемые запросы и производительность»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке C# Academy?

Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Жадная, ленивая и явная загрузка
  2. Сырой SQL, хранимые процедуры и интерполяция
  3. Компилируемые запросы и производительность
  4. Глобальные фильтры запросов и принадлежащие сущности
← Назад к C# Academy