Linq Multiple,基于不同条件 IEnumerable IQueryable 回到您的问题

问题描述

在搜索页面中,我有一些基于它们的选项,搜索查询必须不同。我写了这个:

int userId = Convert.ToInt32(HttpContext.User.Identity.GetUserId());

var followings = (from f in _context.Followers
                  where f.FollowersFollowerId == userId && f.FollowersIsAccept == true
                  select f.FollowersUserId).ToList();

int value;

if (spto.Page == 0)
{
    var post = _context.Posts.AsNoTracking().Where(p => (followings.Contains(p.PostsUserId) || p.PostsUser.UserIsPublic == true || p.PostsUserId == userId) && p.PostIsAccept == true).Select(p => p).AsEnumerable();

    if(spto.MinCost != null)
    {
        post = post.Where(p => int.TryParse(p.PostCost,out value) && Convert.ToInt32(p.PostCost) >= spto.MinCost).Select(p => p);
    }

    if (spto.MaxCost != null)
    {
        post = post.Where(p => int.TryParse(p.PostCost,out value) && Convert.ToInt32(p.PostCost) <= spto.MaxCost).Select(p => p);
    }

    if (spto.TypeId != null)
    {
        post = post.Where(p => p.PostTypeId == spto.TypeId).Select(p => p);
    }

    if (spto.CityId != null)
    {
        post = post.Where(p => p.PostCityId == spto.CityId).Select(p => p);
    }

    if (spto.IsImmidiate != null)
    {
        post = post.Where(p => p.PostIsImmediate == true).Select(p => p);
    }

    var posts = post.Select(p => new
     {
         p.Id,Image = p.PostsImages.Select(i => i.PostImagesImage.ImageAddress).FirstOrDefault(),p.PostCity.CityName,p.PostType.TypeName
     }).AsEnumerable().Take(15).Select(p => p).ToList();

    if (posts.Count != 0)
        return Ok(posts);

    return NotFound();

在这种情况下,我有6个查询需要花费时间,并且性能低下并且代码太长。有什么更好的方法可以编写更好的代码?

解决方法

简短的回答:如果直到最后都没有执行ToListAsEnumerable,那么您将只对dbContext执行一个查询。

因此,保留所有内容IQueryable<...>,直到创建List<...> posts为止:

var posts = post.Select(p => new
 {
     p.Id,Image = p.PostsImages
              .Select(i => i.PostImagesImage.ImageAddress)
              .FirstOrDefault(),p.PostCity.CityName,p.PostType.TypeName,})
 .Take(15)
 .ToList();

IQueryable和IEnumerable

出于跳过所有ToList / AsEnumerable有助于提高性能的原因,您需要了解IEnumerable<...>IQueryable<...>之间的区别。

IEnumerable

实现IEnumerable<...>的类的对象表示枚举该对象可以产生的序列的可能性。

该对象保存一切以产生序列。要求序列后,将由您的本地进程执行代码以产生序列。

在低级,您可以使用GetEnumerator生成序列并重复调用MoveNext。只要MoveNext返回true,序列中就存在下一个元素。您可以使用属性Current访问下一个元素。

枚举序列是这样完成的:

IEnumerable<Customer> customers = ...
using (IEnumarator<Customer> customerEnumerator = customers.GetEnumerator())
{
    while (customerEnumerator.MoveNext())
    {
        // there is still a Customer in the sequence,fetch it and process it
        Customer customer = customerEnumerator.Current;
        ProcessCustomer(customer);
    }
}

这是很多代码,因此C#的创建者发明了foreach,它将执行大部分代码:

foreach (Customer customer in customers)
    ProcessCustomer(customer);

现在您知道foreach后面的代码,您可能会了解foreach第一行中发生的事情。

请记住,IEnumerable<...>应该由您的本地进程处理。 IEnumerable<...>可以调用本地进程可以调用的每个方法。

IQueryable

实现IQueryable<...>的类的对象非常像IEnumerable<...>,它还表示可能产生类似对象的可枚举序列。但是区别是,应该由另一个进程来提供数据。

为此,IQueryable<...>对象包含一个Expression和一个ProviderExpression表示必须以某种通用格式提取哪些数据的公式; Provider知道谁必须提供数据(通常是数据库管理系统),以及使用什么语言与该DBMS通信(通常是SQL)。

只要连接LINQ方法或您自己的仅返回IQueryable<...>的方法,就只会更改Expression。不执行查询,不联系数据库。串联这样的语句是一种快速的方法。

仅当您开始枚举时,无论是使用GetEnumerator / MoveNext / Current进行最低级别的枚举,还是使用foreach进行较高级别的枚举,Expression都会发送给Provider,后者将进行翻译将其转换为SQL并从数据库中获取数据。返回的数据表示为调用方的可枚举序列。

请注意,有一些LINQ方法不返回IQueryable<TResult>,而是返回List<TResult>TResult,bool或int等:ToList / FirstOrDefault / Any / Count / etc. Those methods will deep inside call GetEnumerator / MoveNext /当前`;因此这些方法将从数据库中获取数据。

回到您的问题

数据库管理系统经过优化,可处理数据:提取,排序,过滤等。数据库查询的较慢部分之一是将提取的数据传输到本地进程。

因此,明智的做法是让DBMS进行尽可能多的数据库处理,并且仅将数据传输到您实际打算使用的本地进程中。

因此,如果本地进程不使用提取的数据,请尝试避免使用ToList。在您的情况下:您将followings传输到本地进程,而只是通过IQueryable.Contains方法将其传输回数据库。

此外,(在某种程度上取决于您使用的框架),AsEnumerable会将数据传输到本地进程,因此您的本地进程必须使用Where和{ {1}}。

A,您忘了给我们提供您的要求的描述(“从所有帖子中,仅给我提供那些...”),对我来说,分析您所有的查询内容实在太多了,但是您却获得了如果您尝试将所有Contains保持尽可能长的时间,则效率最高。

IQueryable<...>可能存在一些问题。您的提供程序可能不知道如何将其转换为SQL。可能有几种解决方案:

  • 显然Int.TryParse(...)代表一个数字。考虑将其存储为数字。如果是金额(价格或某物,小数位数有限),请考虑将其存储为小数。
  • 如果您真的不能说服项目负责人将数字存储为小数,请在可以找到合适数据库的工作中进行搜索,或者考虑创建将PostCost中的字符串转换为小数的存储过程/ int。
  • 如果仅使用15个元素,请使用PostCost,而不要使用IQueryable.Take(15)

进一步的优化:

IEnumerable.Take(15)

换句话说:使以下IQueryable成为可能,但尚未执行它:“在所有关注者中,仅保留那些被接受且FollowersFollowerId等于userId的关注者。在其余的Followers中,获取FollowersUserId”。

似乎您仅打算在page为零时使用它。如果页面不为零,为什么还要创建此查询?

顺便说一句,从不使用诸如int userId = var followerUserIds = _context.Followers .Where(follower => follower.FollowersFollowerId == userId && follower.FollowersIsAccept) .Select(follower => follower.FollowersUserId); 之类的语句,甚至不要使用更糟糕的语句:where a == true,这给读者留下了这样的印象:仅使用{{1} }和if (a == true) then b == true else b == false

接下来,您决定创建一个零个或多个帖子的查询,并认为给它一个单数名词作为标识符where a是个好主意。

b = a

post将导致与var post = _context.Posts .Where(post => (followings.Contains(post.PostsUserId) || post.PostsUser.UserIsPublic || post.PostsUserId == userId) && post.PostIsAccept); 表的联接。如果仅将Followed表中的Accepted帖子加入,可能会更有效率。因此,在您决定加入之前,请先检查PostIsAccept和其他谓词:

Contains

所有不被接受的帖子都不必加入以下关注;取决于您的提供者是否足够聪明:它不会加入所有公共用户,也不会加入具有userId的用户,因为它知道它已经通过了过滤器。

考虑使用Followers代替.Where(post => post.PostIsAccept && (post.PostsUser.UserIsPublic || post.PostsUserId == userId || followings.Contains(post.PostsUserId));

在我看来,您需要以下条件:

我有一个UserId;给我所有来自该用户或来自公共用户的,或具有受关注的关注者的已接受帖子

Contains

请注意:我仍然没有执行查询,只是更改了表达式!

在第一个帖子定义之后,您可以根据spto的各种值进一步过滤帖子。您可以考虑进行这一大查询,但是我认为这不会加快该过程。只会使它更不可读。

最后:为什么要使用:

Any

这对您的序列没有任何作用,只会使其变慢。

,

我已经用三元运算符解决了我的问题:

var post = _context.Posts.AsNoTracking().Where(p => 
(followings.Contains(p.PostsUserId) || p.PostsUser.UserIsPublic == true || p.PostsUserId == userId) && p.PostIsAccept == true
&& (spto.MinCost != null ? int.TryParse(p.PostCost,out value) && Convert.ToInt32(p.PostCost) >= spto.MinCost : 1 == 1)
&& (spto.MaxCost != null ? int.TryParse(p.PostCost,out value) && Convert.ToInt32(p.PostCost) <= spto.MaxCost : 1 == 1)
&& (spto.TypeId != null ? p.PostTypeId == spto.TypeId : 1 == 1)
&& (spto.CityId != null ? p.PostCityId == spto.CityId : 1 == 1)
&& (spto.IsImmidiate != null && spto.IsImmidiate == true ? p.PostIsImmediate == true : 1 == 1)).Select(p => new
{
    p.Id,Image = p.PostsImages.Select(i => i.PostImagesImage.ImageAddress).FirstOrDefault(),p.PostType.TypeName
}).Skip(spto.Page * 15).Take(15).ToList();

编辑(更好的代码)

感谢@ZoharPeled,@HaraldCoppoolse,@JonasH我已经更改了代码:

int value;

var post = _context.Posts.AsNoTracking().Where(p =>
    (followings.Contains(p.PostsUserId) || p.PostsUser.UserIsPublic == true || p.PostsUserId == userId) && p.PostIsAccept == true
    && (spto.MinCost == null || (int.TryParse(p.PostCost,out value) && Convert.ToInt32(p.PostCost) >= spto.MinCost))
    && (spto.MaxCost == null || (int.TryParse(p.PostCost,out value) && Convert.ToInt32(p.PostCost) <= spto.MaxCost))
    && (spto.TypeId == null || p.PostTypeId == spto.TypeId)
    && (spto.CityId == null || p.PostCityId == spto.CityId)
    && (spto.IsImmidiate == null || p.PostIsImmediate == true)).Select(p => new
    {
        p.Id,p.PostType.TypeName
    }).Skip(spto.Page * 15).Take(15).ToList();

编辑(最佳代码):

int userId = Convert.ToInt32(HttpContext.User.Identity.GetUserId());

var followings = _context.Followers
                        .Where(follower => follower.FollowersFollowerId == userId
                                        && follower.FollowersIsAccept)
                        .Select(follower => follower.FollowersUserId);

int value;

var post = _context.Posts.AsNoTracking().Where(p => p.PostIsAccept 
                     && (p.PostsUser.UserIsPublic || p.PostsUserId == userId 
                     || _context.Followers.Where(f => f.FollowersFollowerId == userId 
                     && f.FollowersIsAccept).Select(f => f.FollowersUserId).Any()));

if (spto.MinCost != null)
    post = post.Where(p => int.TryParse(p.PostCost,out value) && Convert.ToInt32(p.PostCost) >= spto.MinCost);

if (spto.MaxCost != null)
    post = post.Where(p => int.TryParse(p.PostCost,out value) && Convert.ToInt32(p.PostCost) <= spto.MaxCost);

if (spto.TypeId != null)
    post = post.Where(p => p.PostTypeId == spto.TypeId);

if (spto.CityId != null)
    post = post.Where(p => p.PostCityId == spto.CityId);

if (spto.IsImmidiate != null)
    post = post.Where(p => p.PostIsImmediate == true);

var posts = post.Select(p => new
{
    p.Id,p.PostType.TypeName
}).Skip(spto.Page).Take(15).ToList();

if (posts.Count != 0)
    return Ok(posts);
,

一些观察结果:

.AsEnumerable()

如果要使用自定义集合,则它旨在隐藏运算符的位置。在这种情况下,不需要它。

.Select(p => p)

我看不到有任何目的,将其删除。

int.TryParse(p.PostCost,out value) && Convert.ToInt32(p.PostCost) >= spto.MinCost

解析可能会很昂贵,因此您希望做得尽可能少,如果您同时拥有min和max,那么它会做两次,四次。将其替换为具有值的直接比较,即int.TryParse(p.PostCost,out value) && value >= spo.MinCost。我还建议有一个明确的案例,即同时存在最小成本和最大成本以避免两次解析。

followings.Contains(p.PostsUserId) 

以下内容是一个列表,因此它将搜索所有项目。使用HashSet可以提高性能。即创建以下列表时,将.ToList()替换为ToHashSet()。 HashSet使用哈希表使Contains()成为恒定时间操作而不是线性操作。

查询顺序

您希望订购支票以尽早消除尽可能多的项目,并先进行简单,快速的支票,再执行较慢的支票。

合并运营商

一个操作员通常比多个呼叫要快。

使用普通循环

如果您确实需要尽可能高的性能,则最好使用常规循环。 Linq非常适合编写紧凑的代码,但是使用普通循环通常会提高性能。

个人资料

每当谈到性能时,必须指出profiling的重要性。上面的评论是开始的合理位置,但是可能有些完全不同的事情需要花费时间。唯一知道的方法就是剖析。这也应该为改进提供很好的指示。

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...