加入内存时,LINQ 查询中的“位置”是不是重要?
Posted
技术标签:
【中文标题】加入内存时,LINQ 查询中的“位置”是不是重要?【英文标题】:Does "where" position in LINQ query matter when joining in-memory?加入内存时,LINQ 查询中的“位置”是否重要? 【发布时间】:2018-12-18 11:21:21 【问题描述】:情况:假设我们正在执行一个连接两个内存列表的 LINQ 查询(因此不涉及 DbSet 或 SQL 查询生成),并且该查询还有一个 where
子句。此where
仅过滤原始集合中包含的属性(查询的from
部分)。
问题: linq 查询解释器是否优化了这个查询,因为它首先执行where
,然后再执行join
,不管我是在前面还是后面写where
join
? – 因此它不必对以后不包含的元素执行连接。
示例:例如,我有一个categories
列表,我想加入一个products
列表。但是,我只对category
和ID
1 感兴趣。无论我是否编写,linq 解释器是否在内部执行完全相同的操作:
from category in categories
join prod in products on category.ID equals prod.CategoryID
where category.ID == 1 // <------ below join
select new Category = category.Name, Product = prod.Name ;
或
from category in categories
where category.ID == 1 // <------ above join
join prod in products on category.ID equals prod.CategoryID
select new Category = category.Name, Product = prod.Name ;
以前的研究: 我已经看到this question 但 OP 作者 stated 他/她的问题仅针对生成 SQL 的非内存案例。我对 LINQ 在内存中的两个列表上执行连接非常感兴趣。
更新:这不是 "Order execution of chain linq query" 问题的重复,因为引用的问题显然是指数据库集,而我的问题明确解决了非数据库场景。 (此外,虽然相似,但我不是在这里询问基于导航属性的包含,而是关于“连接”。)
Update2:虽然非常相似,但这也不是 "Is order of the predicate important when using LINQ?" 的重复,因为我明确询问内存中的情况,我看不到明确解决这种情况的引用问题。此外,这个问题有点老了,我实际上对 .NET Core 上下文中的 linq 感兴趣(2012 年不存在),所以我更新了这个问题的标签以反映第二点。
请注意:关于这个问题,我的目标是 linq 查询解释器是否会在后台以某种方式优化此查询,并希望获得对一段文档或源代码的参考,以说明如何这是由 linq 完成的。我不对诸如“没关系,因为两个查询的性能大致相同”之类的答案感兴趣。
【问题讨论】:
是的。没有翻译。 LINQ to Objects 查询按原样执行,不会转换为其他内容。Where()
是一个迭代器,它遍历输入并返回与谓词匹配的任何项目。对于the full framework 和.NET Core,您可以直接查看源代码以了解其实现方式
如果你想要可以忍受的性能,你不应该加入这样的内存列表。您将进行 M*N 比较。您应该创建字典或哈希集来查找具有公共键的条目
Order execution of chain linq query的可能重复
还有Is order of the predicate important when using LINQ?。
@SeM 请参阅我的观点,说明为什么这不是重复的。您的第二个链接非常接近,但没有明确引用内存中的连接。但是,您的评论说服了我在说明我对 dotnet core 上下文中的 linq 感兴趣时提出了尖锐的问题。
【参考方案1】:
LINQ 查询语法将被编译为方法链。有关详细信息,请阅读例如in this question.
第一个 LINQ 查询将被编译为以下方法链:
categories
.Join(
products,
category => category.ID,
prod => prod.CategoryID,
(category, prod) => new category, prod )
.Where(t => t.category.ID == 1)
.Select(t => new Category = t.category.Name, Product = t.prod.Name );
第二个:
categories
.Where(category => category.ID == 1)
.Join(
products,
category => category.ID,
prod => prod.CategoryID,
(category, prod) => new Category = category.Name, Product = prod.Name );
如您所见,第二个查询将导致更少的分配(请注意,只有一个匿名类型与第一个查询中的 2 个相比,并注意在执行查询时将创建多少这些匿名类型的实例)。
此外,很明显,第一个查询将对比第二个(已过滤)更多的数据执行连接操作。
在 LINQ-to-objects 查询的情况下不会有额外的查询优化。
所以第二个版本更可取。
【讨论】:
在 IMO 中,重要的不是匿名类型的数量 - 而是在第二种情况下,我们从一开始就加入了更少的数据。 @JonSkeet,是的,但这很明显。在第一种情况下,我们不仅会加入更多数据,还会造成更大的内存压力。 您可能认为这很明显,但我认为这对 OP 来说不一定是显而易见的。 (或者更确切地说,OP 很可能想知道 LINQ to Objects 是否会自动执行该优化。) @JonSkeet,好的,感谢您指出这一点。我更新了我的答案。【参考方案2】:对于内存列表 (IEnumerables),不应用优化,查询执行是按内存列表的链式顺序进行的。
我还尝试了result
,首先将其转换为IQueryable
,然后应用过滤,但显然对于这张大桌子来说,转换时间相当长。
我对这个案例做了一个快速测试。
Console.WriteLine($"List Row Count = list.Count()");
Console.WriteLine($"JoinList Row Count = joinList.Count()");
var watch = Stopwatch.StartNew();
var result = list.Join(joinList, l => l.Prop3, i=> i.Prop3, (lst, inner) => new lst, inner)
.Where(t => t.inner.Prop3 == "Prop13")
.Select(t => new t.inner.Prop4, t.lst.Prop2);
result.Dump();
watch.Stop();
Console.WriteLine($"Result1 Elapsed = watch.ElapsedTicks");
watch.Restart();
var result2 = list
.Where(t => t.Prop3 == "Prop13")
.Join(joinList, l => l.Prop3, i=> i.Prop3, (lst, inner) => new lst, inner)
.Select(t => new t.inner.Prop4, t.lst.Prop2);
result2.Dump();
watch.Stop();
Console.WriteLine($"Result2 Elapsed = watch.ElapsedTicks");
watch.Restart();
var result3 = list.AsQueryable().Join(joinList, l => l.Prop3, i=> i.Prop3, (lst, inner) => new lst, inner)
.Where(t => t.inner.Prop3 == "Prop13")
.Select(t => new t.inner.Prop4, t.lst.Prop2);
result3.Dump();
watch.Stop();
Console.WriteLine($"Result3 Elapsed = watch.ElapsedTicks");
调查结果:
List Count = 100
JoinList Count = 10
Result1 Elapsed = 27
Result2 Elapsed = 17
Result3 Elapsed = 591
List Count = 1000
JoinList Count = 10
Result1 Elapsed = 20
Result2 Elapsed = 12
Result3 Elapsed = 586
List Count = 100000
JoinList Count = 10
Result1 Elapsed = 603
Result2 Elapsed = 19
Result3 Elapsed = 1277
List Count = 1000000
JoinList Count = 10
Result1 Elapsed = 1469
Result2 Elapsed = 88
Result3 Elapsed = 3219
【讨论】:
感谢您的测试。我刚刚仔细查看了AsQueryable 的实现,它与docs entry 相关:"AsQueryable(IEnumerable) 返回 [...] 一个 IQueryableEnumerableQuery
时,它只会按照链式表达式中所述的顺序简单地执行Where
和Join
- 所以使用AsQueryable
似乎在这里没有提供优化优势。以上是关于加入内存时,LINQ 查询中的“位置”是不是重要?的主要内容,如果未能解决你的问题,请参考以下文章