Как IQueryable преобразует лямбда-выражения в SQL-запрос?

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

Ответ

IQueryable<T> не выполняет лямбда-выражения напрямую. Вместо этого он компилирует их в дерево выражений (Expression Tree), которое представляет собой структуру данных, описывающую логику запроса (фильтрацию, сортировку, проекцию). Это дерево затем передаётся поставщику запросов (query provider), например, Entity Framework Core, который анализирует его и транслирует в специфичный для базы данных SQL.

Пример и сравнение с IEnumerable:

// IQueryable: Трансляция в SQL
IQueryable<User> query = dbContext.Users
    .Where(u => u.Age > 18 && u.IsActive) // => WHERE Age > 18 AND IsActive = 1
    .OrderBy(u => u.LastName)             // => ORDER BY LastName
    .Select(u => new { u.Id, u.Name });   // => SELECT Id, Name FROM Users

// Запрос выполнится в БД только здесь:
var result = query.ToList();

// IEnumerable: Выполнение в памяти
IEnumerable<User> inMemoryQuery = dbContext.Users.AsEnumerable()
    .Where(u => u.Age > 18 && u.IsActive); // ВСЕ строки загружаются в память, фильтр применяется в C#

Ключевые механизмы:

  1. Expression Trees: Лямбда u => u.Age > 18 компилируется не в IL-код, а в объект типа Expression<Func<User, bool>>. Это позволяет анализировать её структуру (параметр u, свойство Age, оператор >, константа 18) во время выполнения.
  2. Поставщик запросов (IQueryProvider): Отвечает за создание и выполнение запроса. EF Core имеет своего провайдера для каждой СУБД (SQL Server, PostgreSQL и т.д.), который знает, как преобразовать операции Where, Select, Join в соответствующие SQL-конструкции.
  3. Отложенное выполнение (Deferred Execution): Запрос не выполняется до момента материализации результатов (ToList(), ToArray(), FirstOrDefault(), Count()).

Важные ограничения:

  • Не вся логика C# может быть переведена в SQL. Например, вызов произвольных методов .Where(u => MyCustomMethod(u.Age)) вызовет ошибку на этапе трансляции.
  • Для сложной логики, непереводимой в SQL, можно сначала отфильтровать данные на стороне БД с помощью IQueryable, а затем доработать в памяти с помощью IEnumerable (AsEnumerable()).