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

Ответ

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()).

Ответ 18+ 🔞

Давай разберём эту штуку, а то многие думают, что IQueryable<T> — это просто умный IEnumerable<T>. Нет, блядь, это принципиально другая хуйня, и если не понимать разницу, можно так наебнуться с производительностью, что мало не покажется.

Смотри, в чём главный фокус. Когда ты пишешь IEnumerable.Where(u => u.Age > 18), лямбда u => u.Age > 18 компилируется в обычный скомпилированный метод, байткод. И когда ты вызываешь ToList(), этот метод просто прогоняется для каждой записи уже в памяти. Всё просто, как три копейки.

А вот IQueryable — это хитрая жопа. Он не выполняет лямбду как код. Вместо этого он её разбирает на части, как конструктор, и строит из них дерево выражений (Expression Tree). Это такая структура данных, которая говорит: «Слушай, тут у нас параметр u, у него есть свойство Age, и мы хотим сравнить его с константой 18 через оператор "больше"».

// Это не просто функция. Это описание будущего запроса.
IQueryable<User> query = dbContext.Users
    .Where(u => u.Age > 18 && u.IsActive) // => Expression<Func<User, bool>>
    .OrderBy(u => u.LastName)             // => ORDER BY LastName
    .Select(u => new { u.Id, u.Name });   // => SELECT Id, Name

И вот это дерево — не выполняется! Оно просто лежит и ждёт своего часа. А потом, когда ты говоришь «хватит умничать, дай результат» (через ToList(), First(), Count()), происходит магия.

Магия называется «поставщик запросов» (IQueryProvider). В случае с Entity Framework, этот провайдер — настоящий полиглот. Он берёт твоё дерево выражений, внимательно его изучает и говорит: «Ага, понял. Надо сделать вот такой SQL».

И он генерирует что-то вроде:

SELECT Id, Name
FROM Users
WHERE Age > 18 AND IsActive = 1
ORDER BY LastName

И этот запрос улетает прямо в базу данных. Вся фильтрация и сортировка происходит на стороне сервера БД, а обратно приходит уже готовый, отфильтрованный результат. Это, блядь, и есть главная сила IQueryable — он выносит логику туда, где её выполнять эффективнее всего.

А теперь смотри, где народ ебётся:

Допустим, у тебя есть метод, который нельзя перевести в SQL.

bool IsPremiumUser(int age) => age > 21 && DateTime.Now.DayOfWeek == DayOfWeek.Friday;

И ты пишешь:

var badQuery = dbContext.Users.Where(u => IsPremiumUser(u.Age)); // ОШИБКА на этапе трансляции!

Провайдер посмотрит на вызов IsPremiumUser и скажет: «Чувак, я понятия не имею, как это перевести на SQL. Иди нахуй». И будет прав.

Что делать? Разделять зоны ответственности. Сначала отфильтруй в БД то, что можно, а потом в памяти — остальное.

// Эта часть уйдёт в SQL и отфильтрует в БД
var baseQuery = dbContext.Users.Where(u => u.Age > 21).AsEnumerable();

// А эта часть выполнится уже в памяти C#
var finalResult = baseQuery.Where(u => DateTime.Now.DayOfWeek == DayOfWeek.Friday).ToList();

Ключевой момент — AsEnumerable(). Он говорит: «Всё, приехали. Дальше работаем как с обычной коллекцией в памяти».

Итог простой, как палка:

  • IQueryable — для запросов к внешнему источнику (БД). Он транслирует логику. Выполняется отложенно.
  • IEnumerable — для работы с данными в памяти. Он выполняет логику сразу.

Если начать тащить всю таблицу в память через IEnumerable, чтобы потом отфильтровать два десятка записей, то можно получить овердохуища проблем с производительностью. Так что думай головой, где строить запрос, а где уже работать с тем, что есть.