Что такое Split Query (разделённый запрос) в Entity Framework Core?

Ответ

Split Query (Разделённый запрос) — это стратегия загрузки связанных данных в Entity Framework Core, при которой один сложный SQL-запрос с несколькими JOIN разбивается на несколько отдельных, более простых запросов. Это решение проблемы "раздувания Cartesian product" (Cartesian Explosion).

Проблема, которую решает Split Query: При использовании Include() и ThenInclude() для загрузки нескольких коллекций EF Core генерирует один запрос с LEFT JOIN. Если у родительской сущности много дочерних записей в каждой коллекции, результирующий набор данных дублируется для каждой комбинации, что приводит к передаче избыточного объема данных по сети и повышенной нагрузке на память.

Пример: Загрузка заказов (Order) с их позициями (OrderItems).

Стандартный запрос (Single Query):

var orders = context.Orders
    .Include(o => o.OrderItems)
    .ToList();

Генерирует SQL с LEFT JOIN, что может привести к дублированию данных строк заказа для каждой позиции.

Запрос с использованием Split Query:

var orders = context.Orders
    .Include(o => o.OrderItems)
    .AsSplitQuery() // Ключевой метод
    .ToList();

EF Core сгенерирует два запроса:

  1. SELECT * FROM Orders
  2. SELECT * FROM OrderItems WHERE OrderId IN (...) (для ID заказов, полученных в первом запросе).

Преимущества:

  • Уменьшение объёма передаваемых данных по сети, так как исключается дублирование столбцов родительской сущности.
  • Повышение производительности для запросов, включающих несколько коллекций с большим количеством данных.

Недостатки и ограничения:

  • Дополнительные round-trips к базе данных. Вместо одного запроса выполняется несколько (N+1 для вложенных коллекций, но EF Core оптимизирует это, используя оператор IN).
  • Отсутствие согласованности на уровне моментального снимка (snapshot). Если данные изменяются между выполнением разделённых запросов, итоговое состояние загруженного графа объектов может быть несогласованным (риск при высокой параллельной нагрузке).
  • Не поддерживается в некоторых сложных сценариях, например, при использовании Include с последующей фильтрацией (Where) по коллекции.

Способы настройки:

  1. Локально для запроса: Использовать метод AsSplitQuery().
  2. Глобально для контекста: Настроить в OnConfiguring метода DbContext.
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
    optionsBuilder.UseSqlServer(connectionString, options => options.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
    }

    Выбор между SingleQuery (по умолчанию) и SplitQuery зависит от конкретной структуры данных и требований к производительности.

Ответ 18+ 🔞

Слушай, вот тебе реальная история про эту вашу Split Query в Entity Framework. Сидишь ты такой, пишешь код, всё вроде красиво — Include, ThenInclude, коллекции в коллекциях... А потом бац — запрос тормозит как черепаха в патруле, а в логах SQL такой, что глаза на лоб лезут.

В чём прикол, а? Представь, что ты грузишь заказы с их позициями. Стандартный подход — это как поехать в магазин за хлебом на КамАЗе. EF делает один здоровенный запрос с кучей JOIN, и если в каждом заказе по 10 позиций, то данные по самому заказу повторяются в каждой строке результата. Это и есть тот самый "Cartesian Explosion" — когда из-за дублей объём данных раздувается до неприличных размеров. Сервер пашет, сеть стонет, память плачет.

И что, выход есть? А то! Split Query — это как раз та штука, которая говорит: "А давайте не будем городить один мега-запрос, а разобьём его на несколько адекватных".

Вот смотри, был у тебя запрос:

var orders = context.Orders
    .Include(o => o.OrderItems)
    .ToList();

Он генерирует SQL с LEFT JOIN. Скучно, предсказуемо, неэффективно.

А теперь добавляешь волшебное заклинание:

var orders = context.Orders
    .Include(o => o.OrderItems)
    .AsSplitQuery() // Вот этот красавец!
    .ToList();

И EF просыпается и делает уже не один, а два запроса:

  1. Сначала: SELECT * FROM Orders — берём все заказы.
  2. Потом: SELECT * FROM OrderItems WHERE OrderId IN (1, 2, 3...) — берём все позиции для этих заказов одним махом.

Чем хорошо? Данных по сети летит в разы меньше, потому что заказ не дублируется для каждой позиции. Особенно кайфуешь, когда коллекций много и они не пустые. Производительность иногда взлетает просто на глазах.

Но не всё так радужно, конечно.

  • Больше походов к базе. Вместо одного удара — несколько. Хотя EF не тупой, он не делает N+1, а использует IN, так что не так страшно.
  • Согласованность может поехать. Если между первым и вторым запросом кто-то успеет изменить данные в базе (ну, мало ли, админ-потеряшка какой), то в твоих объектах окажется "солянка" из состояний в разные моменты времени. Риск, конечно, но часто им можно пренебречь.
  • Не везде работает. Попробуй сделать Include с последующим Where по коллекции — и всё, приехали, Split Query может и не взлететь.

Как включить?

  1. Точечно, прямо в запросе: Как показал выше — .AsSplitQuery().
  2. Глобально, для всего контекста: Настроил один раз и забыл.
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
    optionsBuilder.UseSqlServer(connectionString, options => options.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
    }

Итог? Split Query — это не серебряная пуля, а инструмент. Когда у тебя запрос с несколькими "жирными" коллекциями и он начинает тупить — это твой кандидат. Когда запрос простой или данные мелкие — не заморачивайся, стандартный JOIN справится. Просто знай, что такая фича есть, и ты уже не будешь, как тот чувак, который пытается вывезти диван на велосипеде.