LINQ之路13:LINQ Operators之连接(Joining)
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)的更多相关文章
- LINQ之路16:LINQ Operators之集合运算符、Zip操作符、转换方法、生成器方法
本篇将是关于LINQ Operators的最后一篇,包括:集合运算符(Set Operators).Zip操作符.转换方法(Conversion Methods).生成器方法(Generation M ...
- LINQ之路15:LINQ Operators之元素运算符、集合方法、量词方法
本篇继续LINQ Operators的介绍,包括元素运算符/Element Operators.集合方法/Aggregation.量词/Quantifiers Methods.元素运算符从一个sequ ...
- LINQ之路11:LINQ Operators之过滤(Filtering)
在本系列博客前面的篇章中,已经对LINQ的作用.C# 3.0为LINQ提供的新特性,还有几种典型的LINQ技术:LINQ to Objects.LINQ to SQL.Entity Framework ...
- LINQ之路10:LINQ to SQL 和 Entity Framework(下)
在本篇中,我们将接着上一篇“LINQ to SQL 和 Entity Framework(上)”的内容,继续使用LINQ to SQL和Entity Framework来实践“解释查询”,学习这些技术 ...
- LINQ之路 7:子查询、创建策略和数据转换
在前面的系列中,我们已经讨论了LINQ简单查询的大部分特性,了解了LINQ的支持计术和语法形式.至此,我们应该可以创建出大部分相对简单的LINQ查询.在本篇中,除了对前面的知识做个简单的总结,还会介绍 ...
- [转]LINQ之路系列博客导航
分享一个学习Linq的好博客:Linq之路
- LINQ之路 4:LINQ方法语法
书写LINQ查询时又两种语法可供选择:方法语法(Fluent Syntax)和查询语法(Query Expression). LINQ方法语法是非常灵活和重要的,我们在这里将描述使用链接查询运算符的方 ...
- LINQ之路(3):LINQ扩展
本篇文章将从三个方面来进行LINQ扩展的阐述:扩展查询操作符.自定义查询操作符和简单模拟LINQ to SQL. 1.扩展查询操作符 在实际的使用过程中,Enumerable或Queryable中的扩 ...
- LINQ之路(2):LINQ to SQL本质
LINQ之路(2):LINQ to SQL本质 在前面一篇文章中回顾了LINQ基本语法规则,在本文将介绍LINQ to SQL的本质.LINQ to SQL是microsoft针对SQL Server ...
随机推荐
- 12:Css3的概念和优势
12:Css3的概念和优势 CSS3是css技术的升级版本,CSS3语言开发是朝着模块化发展的.以前的规范作为一个模块实在是太庞大而且比较复杂,所以,把它分解为一些小的模块,更多新的模块也被加入进来. ...
- mybits根据表自动生成 java类和mapper 文件
mybits根据表自动生成 java类和mapper 文件 我这个脑子啊,每次创建新的工程都会忘记是怎么集成mybits怎么生成mapper文件的,so today , I can't write t ...
- swust oj 1015
堆排序算法 1000(ms) 10000(kb) 2631 / 5595 编写程序堆排序算法.按照从小到大的顺序进行排序,测试数据为整数. 输入 第一行是待排序数据元素的个数: 第二行是待排序的数据元 ...
- js设计模式(四)---迭代器模式
定义: 迭代器模式是指提供一种方法,顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示,迭代器模式可以把迭代的过程从业务逻辑中分离出来,使用迭代器模式,即使不关心对象的内部构造,也可以按 ...
- HTTPclient 4.2.2 传参数和文件流
package com.http; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodin ...
- 执行效率做比较,Go、python、java、c#、delphi、易语言等
比较环境,在win7 64位,比较各种语言的整数型运算,下面的比较只作为单项比较.具体方式,40000*40000遍历相加.为了防止编译器优化,生成一个随机数. 1:c#,在NET2.0框架下作为 ...
- 大课深度复盘、解密研发效率之道 | 第42届MPD工作坊成都站日程公布!
互联网时代,随着区块链.大数据.人工智能等技术的快速发展,产品迭代速度飞快.在这样的市场环境下,提升研发效率.降低研发成本,同时支撑业务的快速发展,是每个企业都追求的目标之一. 大中型企业如何快速转型 ...
- LeetCode 12 - 整数转罗马数字 - [简单模拟]
题目链接:https://leetcode-cn.com/problems/integer-to-roman/ 题解: 把 $1,4,5,9,10,40,50, \cdots, 900, 1000$ ...
- C和C指针小记(十五)-结构和联合
1.结构 1.1 结构声明 在声明结构时,必须列出它包含的所有成员.这个列表包括每个成员的类型和名称. struct tag {member-list} variable-list; 例如 //A s ...
- PowerPoint使用技巧
1.右键Group两个元素,可以一起移动: 2.Insert 屏幕输入功能: 3.录制旁白: 4.录制完旁白之后可以生成视频: 5.如果不确定所有引用的组件是否可以在别的机器上使用,可以导出只CD,生 ...