Linq-to-sql 不产生多个外连接?

Posted

技术标签:

【中文标题】Linq-to-sql 不产生多个外连接?【英文标题】:Linq-to-sql not producing multiple outer-joins? 【发布时间】:2011-11-13 04:25:12 【问题描述】:

我有一个关于 linq-to-sql 的奇怪问题,我真的尝试过四处搜索。我正在设计一个 sql 数据库,并且最近刚刚尝试从中检索对象。

问题在于多个连接。我所有的表都使用标识列作为主键。

Db 设计如下:

MasterTable : Id (primary key, identity column, int), 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()));
    

同样的结果 =(

【问题讨论】:

【参考方案1】:

最有可能的是,它正在执行初始查询,然后在第一个查询之后执行投影,然后触发下一组查询。

我认为您需要对这些连接的表进行一些预加载。

查看这些链接:

LINQ: Prefetching data from a second table

http://www.west-wind.com/weblog/posts/2009/Oct/12/LINQ-to-SQL-Lazy-Loading-and-Prefetching(在“DataLoadOptions for Prefetching”标题下)

http://www.davidhayden.com/blog/dave/archive/2007/08/05/LINQToSQLLazyLoadingPropertiesSpecifyingPreFetchWhenNeededPerformance.aspx

【讨论】:

谢谢回复。我很抱歉地说我已经尝试过预取。更新后的帖子【参考方案2】:

至于“为什么”,Linq-to-SQL 可能认为它通过避免多个外连接来使您的查询更好。

假设您要从主表中提取 20 个条目,并且每个从表在主表中的每个条目都有 20 个条目。您将通过外连接在一次往返中拉出 8000 个条目,而不是每次 400 个的两次往返。有一点是做两次往返更便宜。在这种特殊情况下可能不正确,但很有可能如果您以这种方式连接很多表,并且如果您从每个表中提取大量数据,则很可能会很容易倾斜。

您可能还想研究一下 LINQ to SQL 可能在一次往返中使用多个结果集执行两个 SELECT 的可能性。在这种情况下,双语句方法可能会比双外连接快得多。

更新

经过多一点测试,很明显 Jim Wooley 的答案更符合正确的轨道:显然 Linq to SQL 只是决定不急切地加载除您指定的第一个属性之外的任何内容。这也很奇怪,因为它也不是完全延迟加载它。作为查询初始评估的一部分,它会在单独的往返行程中加载每个属性。对我来说,这似乎是 LINQ to SQL 的一个相当大的限制。

【讨论】:

嗯,你可能是对的。它必须是有目的的 - 否则它会太常见而不能成为一个错误。 @user:看起来我最初的评估是错误的。查看我的更新。【参考方案3】:

试试:

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
                     ;

【讨论】:

Same =( 不过很好尝试。我想知道它是否与身份列有关。当我有 guid 和非身份主键时,我从来没有遇到过任何问题。 不,ID 与 Guid 不应该在提取中产生影响。 另一种方法(但丑陋)是将其分成 2 个查询,然后将它们加入代码中。【参考方案4】:

您在使用 LoadOptions 的预取选项并遍历关联而不是显式连接时走在正确的轨道上,但是由于您尝试从 MasterTable 执行多个 1-M 导航,因此您实际上是在创建一个笛卡尔积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 和 Slave1s 之间的左连接,然后延迟加载 Slave2s 以避免在 SQL 的展平结果中 Slave1 和 Slave2 之间的笛卡尔积。

【讨论】:

好的,谢谢!接受的答案,在大多数情况下,避免笛卡尔积可能值得额外的往返(尽管不是在我的 - 因为每个奴隶只有 0..5 个条目)。无论哪种方式,LoadOptions 都被完全忽略 - 我什至无法选择首先左连接的表。

以上是关于Linq-to-sql 不产生多个外连接?的主要内容,如果未能解决你的问题,请参考以下文章

允许同一个表的多个左外连接?

SQL的连接(外连接内连接交叉连接和自连接)

Linq to Sql:多个左外连接

外连接返回连接列的多个副本

LINQ:具有多个条件的左外连接

使用左外连接的多个字段的 LINQ 连接查询