问题描述
在搜索页面中,我有一些基于它们的选项,搜索查询必须不同。我写了这个:
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个查询需要花费时间,并且性能低下并且代码太长。有什么更好的方法可以编写更好的代码?
解决方法
简短的回答:如果直到最后都没有执行ToList
和AsEnumerable
,那么您将只对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
和一个Provider
。 Expression
表示必须以某种通用格式提取哪些数据的公式; 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的重要性。上面的评论是开始的合理位置,但是可能有些完全不同的事情需要花费时间。唯一知道的方法就是剖析。这也应该为改进提供很好的指示。