加入内存时,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 列表。但是,我只对categoryID 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) 返回 [...] 一个 IQueryable,它通过调用 Enumerable 中的等效查询运算符方法来执行查询而不是 Queryable 中的那些。” ... ...看起来在最终解析/执行EnumerableQuery 时,它只会按照链式表达式中所述的顺序简单地执行WhereJoin - 所以使用AsQueryable 似乎在这里没有提供优化优势。

以上是关于加入内存时,LINQ 查询中的“位置”是不是重要?的主要内容,如果未能解决你的问题,请参考以下文章

Linq 加入查看而不具体化,直到过滤

C#中的LINQ

Linq Join 中的大于条件

这是在 .netCore 项目中使用 LINQ 查询连接两个表的正确方法吗?

Linq 查询“帮助”

使用linq进行查询,结果为空时,是怎么判断的呢?