IQueryable VS IEnumerable

在 EFCore 和 Linq 中,一个重要的接口就是 IQueryable,例如 EFCore 的 Where 方法就会返回一个 IQueryable,DbSet 类也实现了这个接口。这个接口有什么用呢?

首先,IQueryable 接口继承了 IEnumerable 接口,所以后者有的功能前者也有,比如它们都表示该对象可以迭代。

那 IQueryable 和 IEnumerable 有什么区别呢?最重要的区别就是查询过滤的时机不同。IEnumerable 通常被用于位于内存的数组或者集合,这时查询是在本机进行的,.NET 在本机内存中对该数组或集合进行数据过滤,返回符合条件的部分。但如果数据并不在本机中,就可能将过滤操作在数据库系统中完成,也可能将过滤操作在本机中完成。

在一般情况下,IQueryable 中的查询条件会被转换为 SQL 语句,然后交给数据库系统执行,这个 SQL 语句就已经包含了数据过滤。在 EFCore 中,我们可以用 Linq 语法来表示查询条件,但这个 Linq 语句其实并没有在本机中运行过,它只是被转换为 SQL 语句交给数据库,然后 EFCore 从数据库拿取过滤后的数据而已。这个是 IQueryable 和 IEnumerable 的最重要的区别。

using (MyDbContext ctx = new MyDbContext())
{
    var books = ctx.Books.Where(e => e.Price > 10);
    foreach (var book in books)
    {
        Console.WriteLine(book.Title);
    }
}

上面这段代码,Where 方法里的查询条件会被翻译为 SQL 语句的一部分,完整的 SQL 语句如下:

SELECT [t].[Id], [t].[Price], [t].[Time], [t].[Title]
FROM [T_Books] AS [t]
WHERE [t].[Price] > 10.0E0

这里使用了 var 类型推断,其实这里 books 的类型是 IQueryable<T>(在这里,T 是 Book)。

如果使用 IEnumerable<T>,那么事情就不一样了。.NET 会先通过 SQL 语句从数据库拿取所有数据,然后在本机中进行过滤。即此时表示查询条件的 Linq 语句会在本机中执行,而不会翻译为 SQL 语句的一部分。通过 AsEnumerable 方法可以将 IQueryable<T> 接口转换为 IEnumerable<T> 接口,由于前者继承了后者,这个转换是安全的。

using (MyDbContext ctx = new MyDbContext())
{
    var books = ctx.Books.AsEnumerable().Where(e => e.Price > 10);
    foreach (var book in books)
    {
        Console.WriteLine(book.Title);
    }
}

EFCore 生成的 SQL 语句如下:

SELECT [t].[Id], [t].[Price], [t].[Time], [t].[Title]
FROM [T_Books] AS [t]

可以看出这只是一个简单的 Select 语句,将整个表都获取了,过滤是在本机进行的。

从这个区别可以看出 IQueryable 的优势。一般情况下,IQueryable 的行为显然更符合预期,因为它可以提高性能、减少网络负担并最大化利用 SQL 语句。而使用 IEnumerable,可能会造成极高的内存占用。除非你希望在本机内存中处理数据,或者过滤条件会给数据库服务器造成极大负担,否则应该使用 IQueryable。

IQueryable 这种在服务器执行数据查询的机制称为 Server Evaluation,而 IEnumerable  这种在客户端执行数据查询的机制称为 Client Evaluation

在 EFCore 中,绝大多数时候的默认行为就是 Server Evaluation,例如 Where 方法的查询。但在顶层投影的时候,部分行为会采用 Client Evaluation,所谓顶层投影,一般是指查询的最后一次 Select 操作。例如下面这段代码:

static async Task Main(string[] args)
{
    using (MyDbContext ctx = new MyDbContext())
    {
        var books = ctx.Books.Where(e => e.Price > 10).Select(e => new { Title = StrTransform(e.Title) });
        foreach (var book in books)
        {
            Console.WriteLine(book.Title);
        }
    }
}

static string StrTransform(string str)
{
    return new string(str.Reverse().ToArray());
}

这段代码实现查询价格大于 10 的书,并通过 Select 操作将书的标题返回,但在投影时调用了一个静态方法将标题反转。显然 EFCore 并不能将这个静态方法翻译为 SQL 语句,将这个静态方法放进 Where 里执行是会报错的。但如果放在最后一次 Select 操作里不会报错,EFCore 会将这个顶层投影拿到本机执行,但前面的 Where 查询依旧会在数据库服务器执行。这段代码生成的 SQL 语句和上面那段是一样的。