Joining

IEnumerable<TOuter>, IEnumerable<TInner>→IEnumerable<TResult>

Operator

说明

SQL语义

Join

应用一种查询策略来匹配两个集合中的元素,产生一个平展的结果集

INNER JOIN

GroupJoin

同上,但是产生一个层次结果集

INNER   JOIN,

LEFT   OUTER JOIN

Join & GroupJoin Arguments

参数

类型

外层/Outer sequence

IEnumerable<TOuter>

内层/Inner sequence

IEnumerable<TInner>

外键选择器/Outer key selector

TOuter => TKey

内键选择器/Inner key selector

TInner => TKey

结果选择器/Result selector

Join:      (TOuter,TInner) =>   TResult

GroupJoin    (TOuter,IEnumerable<TInner>) => TResult

查询表达式语法

                from outer-var in outer-enumerable
join inner-var in inner-enumerable on outer-key-expr equals inner-key-expr
[ into identifier ]

简介

Join和GroupJoin通过匹配两个输入sequence来产生单个输出sequence。Join产生平展结果集,而GroupJoin产生层次结果集。Join和GroupJoin提供了Select和SelectMany的替代策略。

Join和GroupJoin的优点是他们对于本地内存集合的执行更加有效,因为他们开始就把内层sequence装载到一个按键排序的查找器,这样就避免了重复的遍历每一个内层元素。他们的缺点则是他们只提供了inner和left out join的功能,而cross joins和不等连接non-equi joins还是只能通过Select/SelectMany来实现。

对于LINQ to SQL和Entity Framework查询来讲,Join和GroupJoin并没有提供相对于Select和SelectMany的任何真正优化,因为他们只是生成相应的SQL语句,而执行是在数据库引擎中完成的。

下表总结了 每种join策略的差异:

表:连接策略

策略

结果形状

本地查询效率

Inner   joins

Left   outer joins

Cross   joins

Nonequi   joins

Select + SelectMany

Flat

Bad

Yes

Yes

Yes

Yes

Select + Select

Nested

Bad

Yes

Yes

Yes

Yes

Join

Flat

Good

Yes

-

-

-

GroupJoin

Nested

Good

Yes

Yes

-

-

GroupJoin + SelectMany

Flat

Good

Yes

Yes

-

-

Join

Join运算符执行一个inner join,产生一个平展的输出sequence。示范Join的最简单方式是使用LINQ to SQL,下面的示例列出所有的Customers和他们的Purchases:

            IQueryable<string> query =
from c in dataContext.Customers
join p in dataContext.Purchases on c.ID equals p.CustomerID
select c.Name + " bought a " + p.Description; //Result:
Tom bought a Bike
Tom bought a Holiday
Dick bought a Phone
Harry bought a Car

结果与我们使用SelectMany方式查询时是一致的。

要看到Join相对于SelectMany的优势,我们必须先把查询转为本地查询,如下所示:

            //把所有customers和purchases拷贝到数组,以实现本地查询
Customer[] customers = dataContext.Customers.ToArray();
Purchase[] purchases = dataContext.Purchases.ToArray(); var slowQuery = from c in customers
from p in purchases
where c.ID == p.CustomerID
select c.Name + " bought a " + p.Description;
var fastQuery = from c in customers
join p in purchases on c.ID equals p.CustomerID
select c.Name + " bought a " + p.Description;

尽管上面两个查询产生相同的结果,但是使用join的查询快得多,因为它的Enumerable实现把内层collection预先装载到一个按键排序的查找器。

join的查询语法如下:

join inner-var in inner-sequence on outer-key-expr equals inner-key-expr

LINQ中的Join运算符会区分对待outer sequence和inner sequence,语法上看:

  • Outer sequence是输入sequence (本例中是customers).
  • Inner sequence是我们引入的新集合 (本例中是purchases).

Join执行内连接(inner joins),意味着没有任何Purchases的Customers会被排除在结果之外。对于inner joins,我们可以交换查询的inner和outer sequences并得到相同的结果:

             var fastQuery = from p in purchases
join c in customers on p.CustomerID equals c.ID
select c.Name + " bought a " + p.Description;

我们可以在查询中继续添加join子句。比如,如果每个purchase有一个或多个purchase items,我们可以使用如下查询来join purchase items:

                from c in customers
join p in purchases on c.ID equals p.CustomerID // first join
join pi in purchaseItems on p.ID equals pi.PurchaseID // second join
...

purchases是第一个join的inner sequence和第二个join的outer sequence。我们可以通过嵌套的foreach来获得相同的结果(低性能):

            foreach (Customer c in customers)
foreach (Purchase p in purchases)
if (c.ID == p.CustomerID)
foreach (PurchaseItem pi in purchaseItems)
if (p.ID == pi.PurchaseID)
Console.WriteLine(c.Name + "," + p.Price + "," + pi.Detail);

在查询语法中,前一个join引入的变量会保持在作用域之内,就像SelectMany样式查询中的外部范围变量那样,我们还可以在join子句中间插入where和let子句。

对多个键值进行Join

我们可以使用匿名类型来对多个键值进行Join,如下所示:

                from x in sequenceX
join y in sequenceY on new { K1 = x.Prop1, K2 = x.Prop2 }
equals new { K1 = y.Prop3, K2 = y.Prop4 }
...

要使上面的查询正确执行,两个匿名类型的结构必须完全一致,这样编译器把他们对应到同一个实现类型,从而使连接键值彼此兼容。

Join的方法语法

下面的join查询语法

                from c in customers
join p in purchases on c.ID equals p.CustomerID
select new { c.Name, p.Description, p.Price };

对应的方法语法如下:

                customers.Join(         // outer collection
purchases, // inner collection
c => c.ID, // outer key selector
p => p.CustomerID, // inner key selector
(c, p) => new { c.Name, p.Description, p.Price } // result selector
);

结果选择器表达式为输出sequence创建每个element。如果我们需要在数据转换之前添加其他子句比如orderby:

                from c in customers
join p in purchases on c.ID equals p.CustomerID
orderby p.Price
select c.Name + " bought a " + p.Description;

那么在方法语法中,我们必须在结果选择器中手动构建一个临时的匿名类型,在该匿名类型中保存c和p:

                customers.Join(         // outer collection
purchases, // inner collection
c => c.ID, // outer key selector
p => p.CustomerID, // inner key selector
(c, p) => new { c, p }) // result selector
.OrderBy(x => x.p.Price)
.Select(x => x.c.Name + " bought a " + x.p.Description);

通常情况下,join的查询表达式语法更加简洁。

GroupJoin

GroupJoin和Join一样执行连接操作,但它不是返回一个平展的结果集,而是一个层次结构的结果集,使用每个外层element进行分组。除了inner joins,GroupJoin还允许outer joins。GroupJoin的查询语法也与Join相似,只是后面紧跟着into关键字。

            // Here’s the most basic example of GroupJoin:
IEnumerable<IEnumerable<Purchase>> query =
from c in customers
join p in purchases on c.ID equals p.CustomerID
into custPurchases
select custPurchases; // custPurchases is a sequence

直接出现在join子句之后的into关键字会被翻译为GroupJoin,而在select或group子句之后的into表示继续一个查询。虽然他们有一个共同的特征:都引入了一个新的查询变量,但是into关键字的这两种使用方式大不相同,必须引起注意。

其结果是一个包含了多个sequences的sequence,我们可以通过如下方式进行遍历:

            foreach (IEnumerable<Purchase> purchaseSequence in query)
foreach (Purchase p in purchaseSequence)
Console.WriteLine(p.Description);

但是这种方式不是非常有用,原因在于outer sequence并没有到outer customer的引用,所以purchaseSequence虽然是按customer进行分组的,但我们在结果中却失去了customer的相关信息。通常情况下,我们会在数据转换中添加对外部范围变量的引用:

                from c in customers
join p in purchases on c.ID equals p.CustomerID
into custPurchases
select new { CustName = c.Name, custPurchases };

这会得到和下面的Select子查询相同的结果(对于本地查询来说,Select效率不如join):

                from c in customers
select new
{
CustName = c.Name,
custPurchases = purchases.Where(p => c.ID == p.CustomerID)
};

默认情况下,GroupJoin相当于left outer join。要得到inner join(排除没有任何purchases的customers),可以对custPurchases添加过滤条件:

                from c in customers
join p in purchases on c.ID equals p.CustomerID
into custPurchases
where custPurchases.Any()
select ...

GroupJoin的into关键字之后的子句比如where针对subsequence,而不是单个的child elements。如果要对单独的purchases添加条件,必须在join之前调用Where:

                from c in customers
join p in purchases.Where(p2 => p2.Price > )
on c.ID equals p.CustomerID
into custPurchases ...

平展的外连接/Flat outer joins

事情在我们希望得到一个outer join和平展的结果集时会陷入两难的境地。GroupJoin让我们获得outer join;Join给了我们平展的结果集。所以解决方案就是先调用GroupJoin,然后对每个child sequence使用DefaultIfEmpty,最后调用SelectMany来获取平展结果集:

                from c in customers
join p in purchases on c.ID equals p.CustomerID into custPurchases
from cp in custPurchases.DefaultIfEmpty()
select new
{
CustName = c.Name,
Price = cp == null ? (decimal?)null : cp.Price
};

如果custPurchases为空,DefaultIfEmpty将产生一个null值。第二个from子句会被翻译成SelectMany。所以,它会平展输出所有的purchase subsequence,将他们合并到单一的输出sequence。

使用lookups

在Enumerable的实现中,Join和GroupJoin的工作分为两个步骤。首先,他们会把内层sequence装载到一个查找器,然后通过该查找器来查询外层sequence。一个查找器(lookup)是一个分组的sequence并可以通过key来直接访问。或者我们可以把它想象成一个dictionary,其中每个元素是一个sequence和对应的key。

查找器是只读的并通过如下接口定义:

        public interface ILookup<TKey, TElement> :
IEnumerable<IGrouping<TKey, TElement>>, IEnumerable
{
int Count { get; }
bool Contains(TKey key);
IEnumerable<TElement> this[TKey key] { get; }
}

在处理本地集合时,我们甚至可以手动创建和查询lookups来作为join运算符的替代策略。这么做有如下优点:

  • 我们可以在多个查询之间重用同一个查找器(lookup)
  • 对lookup进行查询可以让我们更好的理解Join和GroupJoin的工作方式

ToLookup扩展方法创建一个lookup,下面的代码把所有的purchases装载到一个lookup(用CustomerID作为Key):

            ILookup<int?, Purchase> purchLookup =
purchases.ToLookup(p => p.CustomerID, p => p);

第一个参数选择键值,第二个参数选择作为value值被装载到lookup中的对象。读取一个lookup就像读取一个dictionary,不同之处在于索引器返回的是一个包含多个匹配元素的sequence。下面的代码遍历所有CustomerID为1的purchases:

            foreach (Purchase p in purchLookup[])
Console.WriteLine(p.Description);

通过使用lookup,我们可以让SelectMany/Select查询运行得像Join/GroupJoin查询一样高效。Join相当于在lookup上使用SelectMany:

                from c in customers
from p in purchLookup[c.ID]
select new { c.Name, p.Description, p.Price };

通过添加DefaultIfEmpty可以让上面的查询变成一个outer join:

                from c in customers
from p in purchLookup[c.ID].DefaultIfEmpty()
select new
{
c.Name,
Descript = p == null ? null : p.Description,
Price = p == null ? (decimal?)null : p.Price
};

GroupJoin等价于在数据转换时读取lookup:

                from c in customers
select new
{
CustName = c.Name,
CustPurchases = purchLookup[c.ID]
};

Enumerable实现

通过前面的介绍,现在我们可以来看看Join和GroupJoin在LINQ中的实现了。

下面是Enumerable.Join实现的简化版本(没有null checking):

        public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, TInner, TResult> resultSelector)
{
ILookup<TKey, TInner> lookup = inner.ToLookup(innerKeySelector);
return from outerItem in outer
from innerItem in lookup[outerKeySelector(outerItem)]
select resultSelector(outerItem, innerItem);
}

GroupJoin的实现与Join类似:

        public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, IEnumerable<TInner>, TResult> resultSelector)
{
ILookup<TKey, TInner> lookup = inner.ToLookup(innerKeySelector);
return from outerItem in outer
select resultSelector(outerItem, lookup[outerKeySelector(outerItem)]);
}

LINQ之路13:LINQ Operators之连接(Joining)的更多相关文章

  1. LINQ之路16:LINQ Operators之集合运算符、Zip操作符、转换方法、生成器方法

    本篇将是关于LINQ Operators的最后一篇,包括:集合运算符(Set Operators).Zip操作符.转换方法(Conversion Methods).生成器方法(Generation M ...

  2. LINQ之路15:LINQ Operators之元素运算符、集合方法、量词方法

    本篇继续LINQ Operators的介绍,包括元素运算符/Element Operators.集合方法/Aggregation.量词/Quantifiers Methods.元素运算符从一个sequ ...

  3. LINQ之路11:LINQ Operators之过滤(Filtering)

    在本系列博客前面的篇章中,已经对LINQ的作用.C# 3.0为LINQ提供的新特性,还有几种典型的LINQ技术:LINQ to Objects.LINQ to SQL.Entity Framework ...

  4. LINQ之路10:LINQ to SQL 和 Entity Framework(下)

    在本篇中,我们将接着上一篇“LINQ to SQL 和 Entity Framework(上)”的内容,继续使用LINQ to SQL和Entity Framework来实践“解释查询”,学习这些技术 ...

  5. LINQ之路 7:子查询、创建策略和数据转换

    在前面的系列中,我们已经讨论了LINQ简单查询的大部分特性,了解了LINQ的支持计术和语法形式.至此,我们应该可以创建出大部分相对简单的LINQ查询.在本篇中,除了对前面的知识做个简单的总结,还会介绍 ...

  6. [转]LINQ之路系列博客导航

    分享一个学习Linq的好博客:Linq之路

  7. LINQ之路 4:LINQ方法语法

    书写LINQ查询时又两种语法可供选择:方法语法(Fluent Syntax)和查询语法(Query Expression). LINQ方法语法是非常灵活和重要的,我们在这里将描述使用链接查询运算符的方 ...

  8. LINQ之路(3):LINQ扩展

    本篇文章将从三个方面来进行LINQ扩展的阐述:扩展查询操作符.自定义查询操作符和简单模拟LINQ to SQL. 1.扩展查询操作符 在实际的使用过程中,Enumerable或Queryable中的扩 ...

  9. LINQ之路(2):LINQ to SQL本质

    LINQ之路(2):LINQ to SQL本质 在前面一篇文章中回顾了LINQ基本语法规则,在本文将介绍LINQ to SQL的本质.LINQ to SQL是microsoft针对SQL Server ...

随机推荐

  1. CF3A Shortest path of the king

    The king is left alone on the chessboard. In spite of this loneliness, he doesn't lose heart, becaus ...

  2. yarn配置日志聚合

    [原文地址] 日志聚集是YARN提供的日志中央化管理功能,它能将运行完成的Container/任务日志上传到HDFS上,从而减轻NodeManager负载,且提供一个中央化存储和分析机制.默认情况下, ...

  3. kafka的一些参数

    参考文档: https://blog.csdn.net/fengzheku/article/details/50585972 http://kafka.apache.org/documentation ...

  4. LCA || BZOJ 1602: [Usaco2008 Oct]牧场行走 || Luogu P2912 [USACO08OCT]牧场散步Pasture Walking

    题面:[USACO08OCT]牧场散步Pasture Walking 题解:LCA模版题 代码: #include<cstdio> #include<cstring> #inc ...

  5. CH 2401 - 送礼 - [折半DFS+二分]

    题目链接:传送门 描述 作为惩罚,GY被遣送去帮助某神牛给女生送礼物(GY:貌似是个好差事)但是在GY看到礼物之后,他就不这么认为了.某神牛有N个礼物,且异常沉重,但是GY的力气也异常的大(-_-b) ...

  6. [No0000C6]Visual Studio 2017 函数头显示引用个数

    Visual Studio 2017  函数头显示引用个数

  7. js 循环list

    $.ajax({ type : "POST", data:{ createStartTime:'', createEndTime:'' }, url : "<%=r ...

  8. gitlab备份、恢复、升级

    1.备份 gitlab的备份很简单,只要使用命令: gitlab-rake gitlab:backup:create 即可将当前的数据库.代码全部备份到/var/opt/gitlab/backups ...

  9. 【Tools】-NO.89.Tools.4.Visual Studio 2017.1.001-【Visual Studio 2017 安装与卸载】-

    1.0.0 Summary Tittle:[Tools]-NO.89.Tools.4.Visual Studio 2017.1.001-[Visual Studio 2017 安装与卸载]- Styl ...

  10. JaCoCo在Tomcat服务器上监控代码覆盖率的使用方法

    简介 Jacoco是一个开源的覆盖率工具.Jacoco可以嵌入到Ant .Maven中,并提供了EclEmma Eclipse插件,也可以使用JavaAgent技术监控Java程序.很多第三方的工具提 ...