在左连接上生成额外子查询

时间:2012-10-05 13:37:19

标签: sql sql-server linq linq-to-sql left-join

我使用标准GroupJoin / SelectMany / DefaultIfEmpty方法编写了一个在LINQ中执行左连接的方法:

public static IQueryable<TResult> LeftJoin<TLeft, TRight, TKey, TResult>(
    this IQueryable<TLeft> left,
    IEnumerable<TRight> right,
    Expression<Func<TLeft, TKey>> leftKeySelector,
    Expression<Func<TRight, TKey>> rightKeySelector,
    Expression<Func<TLeft, TRight, TResult>> resultSelector)
{
    var paramL = Expression.Parameter(typeof(TLeft), "l");
    var paramR = Expression.Parameter(typeof(TRight), "r");
    var paramRs = Expression.Parameter(typeof(IEnumerable<TRight>), "rs");

    var expr = Expression.Lambda<Func<TLeft, IEnumerable<TRight>, IEnumerable<TResult>>>(
       Expression.Call(
           typeof(Enumerable),
           "Select",
           new [] { typeof(TRight), typeof(TResult) },
           Expression.Call(typeof(Enumerable), "DefaultIfEmpty", new[] { typeof(TRight) }, paramRs),
           Expression.Lambda<Func<TRight, TResult>>(
               Expression.Invoke(resultSelector, paramL, paramR),
               paramR)),
       paramL,
       paramRs
   );

    return left
        .GroupJoin(
            right,
            leftKeySelector,
            rightKeySelector,
            expr)
        .SelectMany(x => x);
}

我测试了它:

var q = myDB.PurchaseOrderHeaders
    .LeftJoin(
        myDB.PurchaseOrderLines,
        po => po.PurchaseOrderGUID,
        line => line.PurchaseOrderGUID,
        (po, line) => new { PO = po, Line = line }
    );

var e = q.AsEnumerable();

我希望SQL像这样:

SELECT [t0].[PurchaseOrderGUID], ..., [t1].[PurchaseOrderLineGUID], ...
FROM [dbo].[PurchaseOrderHeader] AS [t0]
LEFT OUTER JOIN [dbo].[PurchaseOrderLine] AS [t1]
    ON [t0].[PurchaseOrderGUID] = [t1].[PurchaseOrderGUID]

但得到了这个:

SELECT [t0].[PurchaseOrderGUID], ..., [t2].[test], [t2].[PurchaseOrderLineGUID], ...
FROM [dbo].[PurchaseOrderHeader] AS [t0]
LEFT OUTER JOIN (
    SELECT 1 AS [test], [t1].[PurchaseOrderLineGUID], ...
    FROM [dbo].[PurchaseOrderLine] AS [t1]
    ) AS [t2] ON [t0].[PurchaseOrderGUID] = [t2].[PurchaseOrderGUID]

区别在于SELECT 1 as [test]的子查询。为什么会产生这个?是否可能对性能产生重大影响?如果是这样,我可以修改查询以消除它吗?

2 个答案:

答案 0 :(得分:2)

(免责声明:我对LINQ知之甚少。以下内容基于我对SQL的了解,以及对LINQ尝试做什么的有根据的推断。)

  

为什么会产生这个?

我认为1 AS [test]的目的是为LINQ提供一种清晰,简单,一致且明确的方法来区分“PurchaseOrderLine”中的“匹配记录”与{{1中的一个匹配记录}}”。您可能认为可以通过检查PurchaseOrderLine和其他字段来区分这些,这在您的情况下可能是正确的;但是在一般情况下,如果PurchaseOrderLineGUID成功加入记录,但从该记录中选择的所有字段都为空,会发生什么? (在你的情况下,这是不可能的,因为LEFT JOIN是(我假设)不可空,但LINQ知道吗?虽然就此而言,即使不知道哪些表列是不可为空的,人类的查询-writer可以通过在顶级字段列表中使用PurchaseOrderLineGUID来避免子查询,因为[t2].[PurchaseOrderGuid] AS [test]子句可以防止ON在匹配成功时为空的可能性;但是我不确定这对LINQ来说有多明显。)

  

是否可能对绩效产生重大影响?

不应该是任何人;因为[t2].[PurchaseOrderGuid]不能用于任何真正影响查询语义的地方(例如,在1 AS [test]WHEREONGROUP BY子句中),SQL Server应该能够执行“谓词下推”(在某种意义上)将HAVING条件移动到子查询中,并在ONPurchaseOrderHeader之间执行常规索引散列连接以确定哪个记录它需要。只有在汇总结果集时才会为实际选中的PurchaseOrderLine记录添加1 AS [test]

(我之所以这么说是因为我知道SQL Server擅长谓词下推 - even in the rare cases where that turns out to be a bad thing - 部分原因是,如上所述,LINQ 可以避免创建子查询在这种情况下。我想LINQ团队知道他们在做什么,如果他们认为子查询可能会有性能损失,我猜测LINQ会更加努力地确定给定的案例是否真的需要子查询。 LINQ并不打扰,这可能是因为它并不重要。)

答案 1 :(得分:0)

LinqKit有助于解决这些问题。以下扩展创建了很好的sql。

public static IQueryable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
    this IQueryable<TOuter> outer,
    IQueryable<TInner> inner,
    Expression<Func<TOuter, TKey>> outerKeySelector,
    Expression<Func<TInner, TKey>> innerKeySelector,
    Expression<Func<TOuter, TInner, TResult>> result) {

    return outer.GroupJoin(
            inner, 
            outerKeySelector, 
            innerKeySelector, 
            (a, b) => new { a, b }).AsExpandable()
        .SelectMany(
            z => z.b.DefaultIfEmpty(), 
            (z, b) => result.Invoke(z.a, b));
}