Какие проблемы могут возникнуть при использовании left join с двойными join
C#left joinSQLбазы данныхпроблемы с запросами
Что проверяют
Понимаете ли вы, как соединения множат строки, и замечаете ли это в SQL, который генерирует EF Core. Следом обычно спрашивают про Include нескольких коллекций и AsSplitQuery.
Как отвечать
Сначала SQL:
- декартово произведение:
Customers LEFT JOIN Orders LEFT JOIN Tickets, где заказы и обращения независимо привязаны к клиенту. 5 заказов и 6 обращений дают 30 строк на клиента, суммы завышаются. Решение: агрегировать в подзапросах до соединения; - фильтр в
WHERE: условие на правую таблицу вWHEREвыкидывает строки сNULLи превращаетLEFT JOINвINNER. Фильтр правой стороны ставится вON; - смешанная цепочка:
LEFT JOINк B, а за нимINNER JOINк C по колонке B отрезает всё, где B не нашлось; - NULL в агрегатах:
COUNT(*)считает пустую строку клиента без заказов,COUNT(o.Id)нет.
Теперь EF Core:
Includeдвух коллекций:.Include(c => c.Orders).Include(c => c.Tickets)по умолчанию собирается одним запросом с тем самым декартовым произведением, и EF Core пишет об этом предупреждение в лог.AsSplitQuery()разбивает загрузку на отдельные запросы, глобально это включаетUseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery). Цена: несколько обращений к базе и без транзакции нет согласованного снимка данных;- левое соединение в LINQ: классика это
GroupJoinплюсSelectManyсDefaultIfEmpty(), в .NET 10 появился отдельный операторLeftJoin; - материализация NULL: проекция
o.Totalвdecimalдля клиента без заказов рискует упасть сInvalidOperationException. Приводите явно кdecimal?; - Dapper: при multi-mapping дубли родителя приходится схлопывать самому, через
Dictionaryпо ключу.
Где спотыкаются
Добавляют Distinct() и считают, что проблема ушла, хотя суммы по-прежнему посчитаны по размноженным строкам.
Включают AsSplitQuery везде подряд. Для одной коллекции одиночный запрос обычно быстрее, а разделение даёт лишние обращения к базе.
Проверяют запрос на данных, где у каждого клиента по одному заказу. Размножение видно только на реалистичных объёмах, поэтому смотрите сгенерированный SQL через ToQueryString().