繁体   English   中英

返回 IEnumerable<T> 对比 IQueryable<T>

[英]Returning IEnumerable<T> vs. IQueryable<T>

返回IQueryable<T>IEnumerable<T>之间有什么区别,什么时候应该优先于另一个?

IQueryable<Customer> custs = from c in db.Customers
where c.City == "<City>"
select c;

IEnumerable<Customer> custs = from c in db.Customers
where c.City == "<City>"
select c;

两者都将被推迟执行,何时应该优先于另一个?

是的,两者都会给你延迟执行

不同之处在于IQueryable<T>是允许 LINQ-to-SQL(实际上是 LINQ.-to-anything)工作的接口。 因此,如果您进一步优化对IQueryable<T>查询,则该查询将在可能的情况下在数据库中执行。

对于IEnumerable<T>情况,它将是 LINQ-to-object,这意味着所有与原始查询匹配的对象都必须从数据库加载到内存中。

在代码中:

IQueryable<Customer> custs = ...;
// Later on...
var goldCustomers = custs.Where(c => c.IsGold);

该代码将执行 SQL 以仅选择黄金客户。 另一方面,以下代码将执行数据库中的原始查询,然后过滤掉内存中的非黄金客户:

IEnumerable<Customer> custs = ...;
// Later on...
var goldCustomers = custs.Where(c => c.IsGold);

这是一个非常重要的区别,在很多情况下使用IQueryable<T>从数据库中返回太多行。 另一个主要的例子是分页:如果你在IQueryable上使用TakeSkip ,你只会得到请求的行数; IEnumerable<T>上执行此操作将导致您的所有行都加载到内存中。

最重要的答案是好的,但它没有提到解释两个接口“如何”不同的表达式树。 基本上,有两组相同的 LINQ 扩展。 Where()Sum()Count()FirstOrDefault()等都有两个版本:一个接受函数,一个接受表达式。

  • IEnumerable版本签名为: Where(Func<Customer, bool> predicate)

  • IQueryable版本签名是: Where(Expression<Func<Customer, bool>> predicate)

您可能一直在使用这两种方法而没有意识到,因为它们都是使用相同的语法调用的:

例如Where(x => x.City == "<City>")适用于IEnumerableIQueryable

  • IEnumerable集合上使用Where()时,编译器将编译函数传递给Where()

  • IQueryable集合上使用Where()时,编译器将表达式树传递给Where() 表达式树类似于反射系统,但用于代码。 编译器将您的代码转换成一种数据结构,以易于理解的格式描述您的代码的作用。

为什么要打扰这个表达式树呢? 我只想要Where()过滤我的数据。 主要原因是 EF 和 Linq2SQL ORM 都可以将表达式树直接转换为 SQL,从而使您的代码执行速度更快。

哦,这听起来像是免费的性能提升,在这种情况下我应该AsQueryable()使用AsQueryable()吗? 不, IQueryable只有在底层数据提供者可以用它做一些事情时才有用。 将常规List类的内容转换为IQueryable不会给您带来任何好处。

是的,两者都使用延迟执行。 让我们使用 SQL Server 探查器来说明不同之处....

当我们运行以下代码时:

MarketDevEntities db = new MarketDevEntities();

IEnumerable<WebLog> first = db.WebLogs;
var second = first.Where(c => c.DurationSeconds > 10);
var third = second.Where(c => c.WebLogID > 100);
var result = third.Where(c => c.EmailAddress.Length > 11);

Console.Write(result.First().UserName);

在 SQL Server 探查器中,我们发现一个命令等于:

"SELECT * FROM [dbo].[WebLog]"

针对具有 100 万条记录的 WebLog 表运行该代码块大约需要 90 秒。

因此,所有表记录都作为对象加载到内存中,然后对于每个 .Where() 它将成为内存中针对这些对象的另一个过滤器。

当我们在上面的例子中使用IQueryable而不是IEnumerable时(第二行):

在 SQL Server 探查器中,我们发现一个命令等于:

"SELECT TOP 1 * FROM [dbo].[WebLog] WHERE [DurationSeconds] > 10 AND [WebLogID] > 100 AND LEN([EmailAddress]) > 11"

使用IQueryable运行此代码块大约需要四秒钟。

IQueryable 有一个名为Expression的属性,它存储一个树表达式,当我们在我们的示例中使用result (称为延迟执行)时,它开始创建,最后这个表达式将转换为 SQL 查询以在数据库引擎上运行.

两者都会给你延迟执行,是的。

至于哪个优先于另一个,这取决于您的基础数据源是什么。

返回IEnumerable将自动强制运行时使用 LINQ to Objects 来查询您的集合。

返回IQueryable (顺便说一下,它实现了IEnumerable )提供了额外的功能,可以将您的查询转换为可能在底层源(LINQ to SQL、LINQ to XML 等)上表现更好的东西。

之前已经说了很多,但回到根源,以更技术的方式:

  1. IEnumerable是您可以枚举的内存中对象的集合- 一个内存中的序列,可以进行迭代(在foreach循环中很容易,但您只能使用IEnumerator )。 它们按原样驻留在内存中。
  2. IQueryable是一个表达式树,它会在某个时候被翻译成其他东西,并具有枚举最终结果的能力 我想这是大多数人困惑的地方。

它们显然具有不同的内涵。

IQueryable表示一个表达式树(简单的查询),一旦调用发布 API,它将被底层查询提供程序转换为其他内容,例如 LINQ 聚合函数(Sum、Count 等)或 ToList[Array、Dictionary, ...]。 并且IQueryable对象还实现IEnumerableIEnumerable<T>以便如果它们表示查询,则可以迭代该查询的结果。 这意味着 IQueryable 不必只是查询。 正确的术语是它们是表达式树

现在这些表达式是如何执行的以及它们变成什么都取决于所谓的查询提供程序(我们可以想到的表达式执行程序)。

实体框架世界(即神秘的底层数据源提供程序或查询提供程序)中, IQueryable表达式被转换为本地T-SQL查询。 Nhibernate对它们做了类似的事情。 例如,您可以按照LINQ:构建 IQueryable Provider链接中很好地描述的概念编写自己的一个,并且您可能希望为您的产品商店提供程序服务拥有一个自定义查询 API。

所以基本上, IQueryable对象一直被构建,直到我们显式释放它们并告诉系统将它们重写为 SQL 或其他任何内容并向下发送执行链以进行后续处理。

就像延迟执行一样, LINQ功能可以在内存中保留表达式树方案,并仅在需要时将其发送到执行中,只要针对序列调用某些 API(相同的 Count、ToList 等)。

两者的正确使用在很大程度上取决于您在特定情况下面临的任务。 对于众所周知的存储库模式,我个人选择返回IList ,即列表(索引器等)上的IEnumerable 所以我的建议是只在存储库中使用IQueryable ,而在代码中的任何其他地方使用 IEnumerable。 不是说IQueryable破坏并破坏了关注点分离原则的可测试性问题。 如果您从存储库中返回一个表达式,消费者可以按照他们的意愿使用持久层。

对混乱的一点补充:)(来自评论中的讨论))它们都不是内存中的对象,因为它们本身不是真正的类型,它们是一种类型的标记 - 如果你想深入的话。 但是将 IEnumerables 视为内存中集合而将 IQueryables 视为表达式树是有道理的(这就是为什么即使MSDN这样说)。 重点是 IQueryable 接口继承了 IEnumerable 接口,因此如果它表示一个查询,则可以枚举该查询的结果。 枚举会导致执行与 IQueryable 对象关联的表达式树。 所以,事实上,如果没有内存中的对象,你就不能真正调用任何 IEnumerable 成员。 无论如何,如果它不是空的,它就会进入那里。 IQueryables 只是查询,而不是数据。

一般来说,我会推荐以下内容:

  • 如果您想让开发人员使用您的方法在执行之前优化您返回的查询,请返回IQueryable<T>

  • 如果要传输一组要枚举的对象,请返回IEnumerable

IQueryable想象成它是什么 - 对数据的“查询”(您可以根据需要对其进行优化)。 IEnumerable是一组可以枚举的对象(已经接收或创建)。

通常,您希望保留查询的原始静态类型,直到重要为止。

因此,您可以将变量定义为 'var' 而不是IQueryable<>IEnumerable<>并且您将知道您不会更改类型。

如果您从IQueryable<> ,您通常希望将其保留为IQueryable<>直到有一些令人信服的理由更改它。 这样做的原因是您希望为查询处理器提供尽可能多的信息。 例如,如果您只打算使用 10 个结果(您调用了Take(10) ),那么您希望 SQL Server 知道这一点,以便它可以优化其查询计划并仅向您发送您将使用的数据.

将类型从IQueryable<>更改为IEnumerable<>一个令人信服的原因可能是您正在调用某些扩展函数,而特定对象中的IQueryable<>实现要么无法处理,要么处理效率低下。 在这种情况下,您可能希望将类型转换为IEnumerable<> (例如,通过分配给IEnumerable<>类型的变量或使用AsEnumerable扩展方法),以便您调用的扩展函数最终成为Enumerable类而不是Queryable类。

有一篇包含简要源代码示例的博客文章,说明滥用IEnumerable<T>如何显着影响 LINQ 查询性能: Entity Framework: IQueryable vs. IEnumerable

如果我们深入挖掘并查看源代码,我们可以看到为IEnumerable<T>执行了明显不同的扩展方法:

// Type: System.Linq.Enumerable
// Assembly: System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// Assembly location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Core.dll
public static class Enumerable
{
    public static IEnumerable<TSource> Where<TSource>(
        this IEnumerable<TSource> source, 
        Func<TSource, bool> predicate)
    {
        return (IEnumerable<TSource>) 
            new Enumerable.WhereEnumerableIterator<TSource>(source, predicate);
    }
}

IQueryable<T>

// Type: System.Linq.Queryable
// Assembly: System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// Assembly location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Core.dll
public static class Queryable
{
    public static IQueryable<TSource> Where<TSource>(
        this IQueryable<TSource> source, 
        Expression<Func<TSource, bool>> predicate)
    {
        return source.Provider.CreateQuery<TSource>(
            Expression.Call(
                null, 
                ((MethodInfo) MethodBase.GetCurrentMethod()).MakeGenericMethod(
                    new Type[] { typeof(TSource) }), 
                    new Expression[] 
                        { source.Expression, Expression.Quote(predicate) }));
    }
}

第一个返回可枚举迭代器,第二个通过IQueryable源中指定的查询提供程序创建查询。

我最近遇到了IEnumerable v. IQueryable 所使用的算法首先执行IQueryable查询以获得一组结果。 然后将它们传递给foreach循环,其中项目被实例化为实体框架 (EF) 类。 然后在 Linq to Entity 查询的from子句中使用此 EF 类,导致结果为IEnumerable

我对实体的 EF 和 Linq 还很陌生,所以我花了一段时间才弄清楚瓶颈是什么。 使用 MiniProfiling,我找到了查询,然后将所有单个操作转换为单个IQueryable Linq for Entities 查询。 IEnumerable需要 15 秒, IQueryable执行需要 0.5 秒。 涉及三个表,在阅读本文后,我相信IEnumerable查询实际上形成了一个三表交叉产品并过滤结果。

尝试使用 IQueryables 作为经验法则并分析您的工作以使您的更改可衡量。

“IEnumerable”和“IQueryable”之间的主要区别在于过滤器逻辑的执行位置。 一个在客户端(在内存中)执行,另一个在数据库上执行。

例如,我们可以考虑一个例子,我们的数据库中有 10,000 条记录,假设只有 900 条是活跃用户,所以在这种情况下,如果我们使用“IEnumerable”,那么它首先将所有 10,000 条记录加载到内存中,然后然后对其应用 IsActive 过滤器,最终返回 900 个活动用户。

另一方面,在相同的情况下,如果我们使用“IQueryable”,它将直接在数据库上应用 IsActive 过滤器,该过滤器将直接从那里返回 900 个活动用户。

由于看似相互矛盾的反应(主要是围绕 IEnumerable),我想澄清一些事情。

(1) IQueryable扩展了IEnumerable接口。 (您可以将IQueryable发送到期望IEnumerable不会出错的内容。)

(2) IQueryableIEnumerable LINQ 在迭代结果集时都尝试延迟加载。 (请注意,可以在每种类型的接口扩展方法中看到实现。)

换句话说, IEnumerables并不完全是“内存中”。 IQueryables并不总是在数据库上执行。 IEnumerable必须将东西加载到内存中(一旦被检索,可能是懒惰的),因为它没有抽象数据提供者。 IQueryables依赖于抽象提供程序(如 LINQ-to-SQL),尽管这也可能是 .NET 内存中提供程序。

示例用例

(a) 从 EF 上下文中检索记录列表作为IQueryable (内存中没有记录。)

(b) 将IQueryable传递给模型为IEnumerable的视图。 (有效。 IQueryable扩展IEnumerable 。)

(c) 从视图中迭代并访问数据集的记录、子实体和属性。 (可能会导致异常!)

可能的问题

(1) IEnumerable尝试延迟加载并且您的数据上下文已过期。 由于提供程序不再可用而引发异常。

(2) 实体框架实体代理已启用(默认),并且您尝试访问具有过期数据上下文的相关(虚拟)对象。 与 (1) 相同。

(3) 多个活动结果集 (MARS)。 如果您在foreach( var record in resultSet )块中迭代IEnumerable并同时尝试访问record.childEntity.childProperty ,由于数据集和关系实体的延迟加载,您最终可能会record.childEntity.childProperty MARS。 如果您的连接字符串中未启用,这将导致异常。

解决方案

  • 我发现在连接字符串中启用 MARS 工作不可靠。 我建议你避免使用 MARS,除非它被很好地理解和明确需要。

通过调用resultList = resultSet.ToList()执行查询并存储结果 这似乎是确保您的实体在内存中的最直接方法。

在您访问相关实体的情况下,您可能仍需要数据上下文。 或者,您可以禁用实体代理并从您的DbSet明确Include相关实体。

我们可以以相同的方式使用它们,它们只是性能不同。

IQueryable 仅以有效的方式对数据库执行。 这意味着它创建了一个完整的选择查询并且只获取相关记录。

例如,我们想要取名字以“Nimal”开头的前 10 个客户。 在这种情况下,选择查询将生成为select top 10 * from Customer where name like 'Nimal%'

但是,如果我们使用 IEnumerable,查询将类似于select * from Customer where name like 'Nimal%'和前十名将在 C# 编码级别进行过滤(它从数据库中获取所有客户记录并将它们传递给 C#) .

除了前 2 个非常好的答案(由 driis 和 Jacob 撰写):

IEnumerable 接口位于 System.Collections 命名空间中。

IEnumerable 对象表示内存中的一组数据,并且只能向前移动这些数据。 IEnumerable 对象所代表的查询会立即完整地执行,因此应用程序可以快速接收数据。

执行查询时,IEnumerable 会加载所有数据,如果我们需要对其进行过滤,过滤本身是在客户端完成的。

IQueryable 接口位于 System.Linq 命名空间中。

IQueryable 对象提供对数据库的远程访问,并允许您以从头到尾的直接顺序或以相反顺序浏览数据。 在创建查询的过程中,返回的对象是IQueryable,对查询进行了优化。 因此,它在执行期间消耗的内存更少,网络带宽更少,但同时它的处理速度可能比返回 IEnumerable 对象的查询稍微慢一些。

选择什么?

如果需要完整的返回数据集,那么最好使用 IEnumerable,它提供了最大的速度。

如果您不需要整个返回数据集,而只需要一些过滤后的数据,那么最好使用 IQueryable。

除了上述之外,有趣的是,如果您使用IQueryable而不是IEnumerable ,您可以获得异常:

如果productsIEnumerable则以下工作正常:

products.Skip(-4);

但是,如果products是一个IQueryable并且它试图从数据库表访问记录,那么您将收到此错误:

OFFSET 子句中指定的偏移量不能为负。

这是因为构造了以下查询:

SELECT [p].[ProductId]
FROM [Products] AS [p]
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS

并且 OFFSET 不能有负值。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM