Linq-to-sql не создает несколько внешних соединений?

У меня странная проблема с linq-to-sql, и я действительно пытался ее найти. Я разрабатываю базу данных sql и только что недавно попытался извлечь из нее объект.

Проблема в нескольких соединениях. Во всех моих таблицах в качестве первичных ключей используются идентификационные столбцы.

Db разработан следующим образом:

MasterTable: Id (первичный ключ, столбец идентификатора, целое число), MasterColumn1 (nvarchar (50))

Slave1: Id (первичный ключ, столбец идентификатора, int), MasterId (int, первичный ключ -> MasterTable Id), SlaveCol1

Slave2: Id (первичный ключ, столбец идентификатора, int), MasterId (int, первичный ключ -> MasterTable Id), SlaveColumn2

используемый код:

var db = new TestDbDataContext() { Log = Console.Out };
var res = from f in db.MasterTables
          where f.MasterColumn1 == "wtf"
          select new
                     {
                         f.Id, 
                         SlaveCols1 = f.Slave1s.Select(s => s.SlaveCol1),
                         SlaveCols2 = f.Slave2s.Select(s => s.SlaveColumn2)
                     };
foreach (var re in res)
{
    Console.Out.WriteLine(
        re.Id + " "
      + string.Join(", ", re.SlaveCols1.ToArray()) + " "
      + string.Join(", ", re.SlaveCols2.ToArray())
    );
}

И журнал:

SELECT [t0].[Id], [t1].[SlaveCol1], (
   SELECT COUNT(*)
   FROM [FR].[Slave1] AS [t2]
   WHERE [t2].[MasterId] = [t0].[Id]
   ) AS [value]
FROM [FR].[MasterTable] AS [t0]
LEFT OUTER JOIN [FR].[Slave1] AS [t1] ON [t1].[MasterId] = [t0].[Id]
WHERE [t0].[MasterColumn1] = @p0
ORDER BY [t0].[Id], [t1].[Id]
-- @p0: Input NVarChar (Size = 3; Prec = 0; Scale = 0) [wtf]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.5420
SELECT [t0].[SlaveColumn2]
   FROM [FR].[Slave2] AS [t0]
   WHERE [t0].[MasterId] = @x1
-- @x1: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.5420
1 SlaveCol1Wtf SlaveCol2Wtf

Почему, почему вместо этого не выполняется два внешних соединения? Я действительно забочусь об этом, потому что у меня гораздо больше БД со многими таблицами, относящимися к одной и той же таблице (все они имеют отношение «один ко многим»), и наличие 20 циклов выборки на сервер базы данных не является оптимальным!

В качестве примечания. Я могу получить желаемый результат, используя явные внешние соединения, например:

var db = new TestDbDataContext() { Log = Console.Out };
var res = from f in db.MasterTables
          join s1 in db.Slave1s on f.Id equals s1.MasterId into s1Tbl
          from s1 in s1Tbl.DefaultIfEmpty()
          join s2 in db.Slave2s on f.Id equals s2.MasterId into s2Tbl
          from s2 in s2Tbl.DefaultIfEmpty()
          where f.MasterColumn1 == "wtf"
          select new { f.Id, s1.SlaveCol1, s2.SlaveColumn2 };
foreach (var re in res)
{
    Console.Out.WriteLine(re.Id + " " + re.SlaveCol1 + " " + re.SlaveColumn2);
}

Но я хочу использовать ссылки, которые предоставляет Linq-To-Sql, а не ручные соединения! Как?

----------- редактировать -----------------

Я также пробовал такую ​​предварительную выборку:

using (new DbConnectionScope())
{
    var db = new TestDbDataContext() { Log = Console.Out };
    DataLoadOptions loadOptions = new DataLoadOptions();
    loadOptions.LoadWith<MasterTable>(c => c.Slave1s);
    loadOptions.LoadWith<MasterTable>(c => c.Slave2s);
    db.LoadOptions = loadOptions;

    var res = from f in db.MasterTables
              where f.MasterColumn1 == "wtf"
              select f;
    foreach (var re in res)
    {
        Console.Out.WriteLine(re.Id + " " + 
            string.Join(", ", re.Slave1s.Select(s => s.SlaveCol1).ToArray()) + " " + 
            string.Join(", ", re.Slave2s.Select(s => s.SlaveColumn2).ToArray()));
    }
}

тот же результат = (


person Goff    schedule 08.09.2011    source источник


Ответы (4)


Вы на правильном пути с опцией предварительной выборки с использованием LoadOptions и обходом ассоциаций, а не явных объединений, однако, поскольку вы пытаетесь выполнить несколько переходов 1-M из своего MasterTable, вы эффективно создаете декартово произведение между Slave1 и Slave2 записи. В результате LINQ to SQL игнорирует ваши параметры загрузки и лениво загружает дочерние записи для каждого из ваших дочерних элементов.

Вы можете немного оптимизировать это, удалив второй вариант дочерней нагрузки. Сгенерированный запрос теперь будет выполнять один запрос, возвращающий ваши MasterTable и Slave1, но затем лениво загружает каждый из Slave2. Вы должны увидеть то же самое, если сделаете следующее:

var res = from f in db.MasterTables
          where f.MasterColun1 == "wtf"
          select new 
          {
             f.Id,
             Cols1 = f.Slave1s.Select(s => s.SlaveCol1).ToArray()
             Cols2 = f.Slave2s.Select(s => s.SlaveColumn2).ToArray()
          }

Вы должны увидеть левое соединение между MasterTables и Slave1, а затем ленивую загрузку Slave2, чтобы избежать декартова произведения между Slave1 и Slave2 в сглаженных результатах SQL.

person Jim Wooley    schedule 08.09.2011
comment
Хорошо, спасибо! Принятый ответ, избегание декартова произведения может стоить дополнительных обходов в большинстве случаев (хотя и не в моем случае - потому что каждое ведомое устройство имеет только 0..5 записей). LoadOptions полностью игнорируются в любом случае - я даже не могу выбрать, к какой таблице присоединиться первым. - person Goff; 09.09.2011

Что касается того, почему, Linq-to-SQL, вероятно, считает, что он улучшает ваш запрос, избегая множественных внешних соединений.

Предположим, вы извлекаете 20 записей из главной таблицы, и каждая подчиненная таблица имеет 20 записей на запись в главной таблице. Вы потянете 8000 записей по проводу за один круговой обход с внешним соединением, в отличие от двух круговых обходов с 400 за штуку. Наступает момент, когда дешевле сделать две поездки туда и обратно. Это может быть неверно в данном конкретном случае, но есть большая вероятность, что если вы таким образом объедините очень много таблиц и вытащите много данных для каждой таблицы, это может очень легко склонить чашу весов.

Вы также можете изучить возможность того, что LINQ to SQL может выполнять оба оператора SELECT за один цикл приема-передачи с использованием нескольких наборов результатов. В этом случае подход с двумя операторами, вероятно, будет намного быстрее, чем двойное внешнее соединение.

Обновлять

После небольшого дополнительного тестирования становится очевидным, что ответ Джима Вули более правильный: очевидно, Linq to SQL просто решает не загружать с нетерпением любое указанное вами свойство, кроме первого. Это тоже странно, потому что это тоже не совсем ленивая загрузка. Он загружает каждое свойство в отдельном цикле приема-передачи как часть первоначальной оценки вашего запроса. Мне это кажется довольно значительным ограничением LINQ to SQL.

person StriplingWarrior    schedule 08.09.2011
comment
Хм, наверное, ты прав. Это должно быть намеренно - иначе это было бы слишком часто, чтобы стать ошибкой. - person Goff; 08.09.2011
comment
@user: Похоже, моя первоначальная оценка была неправильной. Смотрите мое обновление. - person StriplingWarrior; 08.09.2011

Скорее всего, он выполняет первоначальный запрос, а затем выполняет проекцию после этого первого запроса, который затем запускает следующий набор запросов.

Я думаю, вам нужно будет выполнить предварительную загрузку этих объединенных таблиц.

Смотрите эти ссылки:

person CodingWithSpike    schedule 08.09.2011
comment
Спасибо за ответ. Извините, но я уже пробовал выполнить предварительную загрузку. См обновленный пост - person Goff; 08.09.2011

Пытаться:

var res = from f in db.MasterTables
          where f.MasterColumn1 == "wtf"
          let s1 = f.Slave1s.Select(s => s.SlaveCol1)
          let s2 = f.Slave2s.Select(s => s.SlaveColumn2)
          select new {
                         f.Id, 
                         SlaveCols1 = s1,
                         SlaveCols2 = s2
                     };
person leppie    schedule 08.09.2011
comment
Same = (Хорошая попытка. Интересно, имеет ли это какое-то отношение к столбцам идентификации. У меня никогда не было проблем, когда у меня были guids и первичные ключи без идентификации. - person Goff; 08.09.2011
comment
Нет, ID и Guid не должны влиять на выборку. - person Jim Wooley; 08.09.2011
comment
Другой способ (но уродливый) - разделить его на 2 запроса, а затем объединить их в коде. - person leppie; 08.09.2011