我使用标准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]
的子查询。为什么会产生这个?是否可能对性能产生重大影响?如果是这样,我可以修改查询以消除它吗?
答案 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]
或WHERE
或ON
或GROUP BY
子句中),SQL Server应该能够执行“谓词下推”(在某种意义上)将HAVING
条件移动到子查询中,并在ON
和PurchaseOrderHeader
之间执行常规索引散列连接以确定哪个记录它需要。只有在汇总结果集时才会为实际选中的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));
}