Повышаем Производительность с Помощью Разделения Запросов в EF Core
Некоторые сложные многотабличные запросы могут выполняться слишком долго в зависимости от количества данных в таблицах.
Допустим, у нас есть таблица заказов Orders. Заказ может иметь одну или несколько позиций LineItems. Кроме того, для позиций заказов есть таблица, содержащая допустимые измерения — LineItemDimensions. Вот запрос, который нам нужен:
dbContextEF Core преобразует это в следующий SQL-запрос:
.Orders
.Include(o => o.LineItems)
.ThenInclude(li => li.Dimensions)
.First(o => o.Id == orderId);
SELECT o.*, li.*, d.*В большинстве случаев этот запрос будет выполняться нормально. Однако здесь мы сталкиваемся с проблемой декартова взрыва - ситуацией, когда на выбор N заказов с M позициями и K измерениями база данных возвращает N*M*K строк.
FROM Orders o
LEFT JOIN LineItems li ON li.OrderId = o.Id
LEFT JOIN LineItemDimensions d ON d.LineItemId = li.Id
WHERE o.Id = @orderId
ORDER BY o.Id, li.Id, d.Id;
В EF Core 5.0 появилась новая функция разделения запросов. Она позволяет указать, что данный LINQ-запрос должен быть разделён на несколько SQL-запросов. Всё, что нужно сделать, это вызвать метод AsSplitQuery:
dbContextВ этом случае EF Core сгенерирует следующие SQL-запросы:
.Orders
.Include(o => o.LineItems)
.ThenInclude(li => li.Dimensions)
.AsSplitQuery()
.First(o => o.Id == orderId);
SELECT o.*Обратите внимание, что для каждого оператора Include у нас есть отдельный запрос. Преимущество здесь в том, что мы не выбираем лишние данные, как это было в предыдущем случае.
FROM Orders o
WHERE o.Id = @orderId;
SELECT li.*
FROM LineItems li
JOIN Orders o ON li.OrderId = o.Id
WHERE o.Id = @orderId;
SELECT d.*
FROM LineItemDimensions d
JOIN LineItems li ON d.LineItemId = li.Id
JOIN Orders o ON li.OrderId = o.Id
WHERE o.Id = @orderId;
Вы можете включить разделение запросов на уровне контекста, вызвав метод UseQuerySplittingBehavior:
services.AddDbContext<ApplicationDbContext>(opt =>Это приведет к тому, что все запросы, создаваемые EF Core, будут разделёнными запросами. Чтобы вернуться к единому SQL-запросу в этом случае, в LINQ-запрос нужно будет добавить AsSingleQuery().
opt.UseSqlServer(
"CONNECTION_STRING",
o => o.UseQuerySplittingBehavior(
QuerySplittingBehavior.SplitQuery)));
Особенности
- Не существует гарантии согласованности для нескольких SQL-запросов. Вы можете столкнуться с проблемой, если одновременно с запросом данных выполняется параллельное обновление. Чтобы смягчить это, можно обернуть запросы в транзакцию, но это также может привести к проблемам с производительностью.
- Каждый запрос требует отдельного обращения к БД по сети. Это может снизить производительность, если задержка для базы высока.
Источник: https://www.milanjovanovic.tech/blog/entity-framework-query-splitting