优化实体框架查询

时间:2014-03-04 01:39:07

标签: c# performance entity-framework

我正在尝试在我自己的时间制作一个stackoverflow克隆来学习EF6和MVC5,我目前正在使用OWin进行身份验证。

当我有50-60个问题时,一切正常,我使用了Red Gate data generator并试图通过成千上万的子表行来增加到100万个问题而没有任何关系只是为了“压力”ORM a位。以下是linq的样子

var query = ctx.Questions
               .AsNoTracking()     //read-only performance boost.. http://visualstudiomagazine.com/articles/2010/06/24/five-tips-linq-to-sql.aspx
               .Include("Attachments")                                
               .Include("Location")
               .Include("CreatedBy") //IdentityUser
               .Include("Tags")
               .Include("Upvotes")
               .Include("Upvotes.CreatedBy")
               .Include("Downvotes")
               .Include("Downvotes.CreatedBy")
               .AsQueryable();

if (string.IsNullOrEmpty(sort)) //default
{
    query = query.OrderByDescending(x => x.CreatedDate);
}
else
{
    sort = sort.ToLower();
    if (sort == "latest")
    {
        query = query.OrderByDescending(x => x.CreatedDate);
    }
    else if (sort == "popular")
    {
        //most viewed
        query = query.OrderByDescending(x => x.ViewCount);
    }
}

var complaints = query.Skip(skipCount)
                      .Take(pageSize)
                      .ToList(); //makes an evaluation..

毋庸置疑,我正在获得SQL超时,并在安装Miniprofiler之后,查看生成的sql语句,这是一个可怕的几百行。

我知道我加入/包含太多表格,但在现实生活中有多少项目,我们只需加入1或2个表格?可能有些情况我们必须用数百万行来做这么多连接,是唯一的存储过程吗?

如果是这样的话,EF本身是否只适合小规模项目?

5 个答案:

答案 0 :(得分:16)

您遇到的问题很可能是Cartesian product

仅基于一些样本数据:

var query = ctx.Questions // 50 
  .Include("Attachments") // 20                                
  .Include("Location") // 10
  .Include("CreatedBy") // 5
  .Include("Tags") // 5
  .Include("Upvotes") // 5
  .Include("Upvotes.CreatedBy") // 5
  .Include("Downvotes") // 5
  .Include("Downvotes.CreatedBy") // 5

  // Where Blah
  // Order By Blah

这将返回

以上的行数
50 x 20 x 10 x 5 x 5 x 5 x 5 x 5 x 5 = 156,250,000

说真的......这是要返回的INSANE行数。

如果您遇到此问题,确实有两种选择:

第一种:简单的方法,依靠Entity-Framework在进入上下文时自动连接模型。然后,使用实体AsNoTracking()并处理上下文。

// Continuing with the query above:

var questions = query.Select(q => q);
var attachments = query.Select(q => q.Attachments);
var locations = query.Select(q => q.Locations);

这将为每个表发出一个请求,但不是156百万行,而是只下载110行。但很酷的部分是它们都连接在EF Context Cache内存中,所以现在questions变量已完全填充。

第二:Create a stored procedure that returns multiple tables and have EF materialize the classes

答案 1 :(得分:10)

我没有看到您的LINQ查询有任何明显错误(.AsQueryable()不应该是强制性的,但如果删除它则不会改变任何内容)。当然,不包含不必要的导航属性(每个都添加一个SQL JOIN),但如果一切都是必需的,那么 就可以了。

现在,当C#代码看起来没问题时,是时候看看生成的SQL代码了。正如您所做的那样,第一步是检索执行的SQL查询。有.Net ways of doing it,对于SQL Server,我个人总是开始SQL Server profiling session

获得SQL查询后,尝试直接对数据库执行,不要忘记包含actual execution plan。这将在大多数情况下准确显示查询的哪个部分。它甚至会指示您是否有明显丢失的索引。

现在的问题是,如果您添加所有这些索引,您的SQL Server会告诉您它们丢失了吗?不必要。请参阅示例Don't just blindly create those missing indexes。您必须选择应添加哪些索引,而不应添加。

由于代码优先方法为您创建了索引,我假设它们只是主键和外键的索引。这是一个好的开始,但这还不够。我不知道你的表中的行数,但是一个明显的索引只有你可以添加(没有代码生成工具可以做到这一点,因为它与你的业务查询有关),是例如,CreatedDate列上的索引,因为您按此值排序商品。如果不这样做,SQL Server将不得不在1M行上执行表扫描,这在性能方面当然是灾难性的。

所以:

  • 如果可以
  • 尝试删除一些Include
  • 查看实际执行计划以查看 是查询中的性能问题
  • 只添加有意义的缺失索引,具体取决于您从数据库中获取/过滤数据的方式

答案 2 :(得分:5)

正如您所知,Include方法会生成可怕的SQL。

免责声明:我是该项目的所有者Entity Framework Plus (EF+)

EF + Query IncludeOptimized方法允许优化生成的SQL,就像EF Core一样。

生成多个SQL(每个包含一个SQL),而不是生成一个可怕的SQL。此功能也作为奖励,它允许过滤相关实体。

文档:EF+ Query IncludeOptimized

var query = ctx.Questions
               .AsNoTracking()
               .IncludeOptimized(x => x.Attachments)                                
               .IncludeOptimized(x => x.Location)
               .IncludeOptimized(x => x.CreatedBy) //IdentityUser
               .IncludeOptimized(x => x.Tags)
               .IncludeOptimized(x => x.Upvotes)
               .IncludeOptimized(x => x.Upvotes.Select(y => y.CreatedBy))
               .IncludeOptimized(x => x.Downvotes)
               .IncludeOptimized(x => x.Downvotes.Select(y => y.CreatedBy))
               .AsQueryable();

答案 3 :(得分:1)

查看Microsoft的this document部分8.2.2:

  

8.2.2多个包含的性能问题

     

当我们听到涉及服务器响应时间的性能问题时   问题,问题的根源是多次查询   包含声明。虽然在查询中包含相关实体   功能强大,了解其下发生的事情非常重要   覆盖。

     

使用多个Include进行查询需要相对较长的时间   在其中的语句通过我们的内部计划编译器来生成   商店命令。大部分时间都花在了尝试上   优化生成的查询。生成的store命令将包含   每个Include的外部联接或联盟,具体取决于您的映射。   像这样的查询将带来你的大型连接图   单个有效负载中的数据库,可以消除任何带宽   问题,特别是当有效载荷中存在大量冗余时   (即包含多个级别的Include遍历关联   一对多方向。

     

您可以检查查询过多返回的情况   通过使用访问查询的基础TSQL来获取大型有效负载   ToTraceString并在SQL Server Management中执行store命令   Studio可以查看有效负载大小。在这种情况下,您可以尝试减少   查询中包含语句的数量只是为了引入   你需要的数据。或者您可以将查询分解为更小的查询   子查询序列,例如:

     

在断开查询之前:

using (NorthwindEntities context = new NorthwindEntities()) {
var customers = from c in context.Customers.Include(c => c.Orders)
                where c.LastName.StartsWith(lastNameParameter)
                select c;

foreach (Customer customer in customers)
{
    ...
} }
     

打破查询后

using (NorthwindEntities context = new NorthwindEntities()) {
var orders = from o in context.Orders
             where o.Customer.LastName.StartsWith(lastNameParameter)
             select o;

orders.Load();

var customers = from c in context.Customers
                where c.LastName.StartsWith(lastNameParameter)
                select c;

foreach (Customer customer in customers)
{
    ...
} }
     

这仅适用于跟踪查询,因为我们正在使用   上下文必须执行身份解析和关联的能力   自动修复。

     

与延迟加载一样,权衡将是更小的查询   有效载荷。您还可以使用各个属性的投影   明确地只选择每个实体所需的数据,但是你   在这种情况下不会加载实体,并且不会进行更新   支撑。

答案 4 :(得分:1)

我不同意Ken2k的回答,并且感到惊讶的是,它获得了与之相当多的支持。

从编译的角度来看,代码可能很好,但是如果您关心查询的性能,那么包含这么多的包含绝对不是可以的。 See 8.2.2 of MSFT's EF6 Performance Whitepaper

  

当我们听到涉及服务器响应时间问题的性能问题时,问题的根源是经常使用多个包含语句的查询

看看EF急切地在一个查询中加载许多导航属性(通过大量的.include()语句)所生成的TSQL,这将很明显说明为什么这样做不好。在一个查询中,最终将产生太多EF生成的联接。

中断查询,以使每个表提取中最多包含2个.include()语句。您可以为每个数据集单独执行.Load(),但很可能不需要那么远,YMMV。

var query = ctx.Questions.Where(...);
// Loads Questions, Attachments, Location tables
query.Include(q => q.Attachments)
     .Include(q => q.Location)
     .Load();

// Loads IdentityUsers Table
query.Select(q => q.CreatedBy).Load();
// Loads Tags
query.Select(q => q.Tags).Load();

// Loads Upvotes and Downvotes
query.Include(q => q.Upvotes)
     .Include(q => q.Downvotes)
     .Load();

// Assuming Upvotes.CreatedBy and Downvotes.CreatedBy are also an IdentityUser,
// then you don't need to do anything further as the IdentityUser table is loaded
// from query.Select(q => q.CreatedBy).Load(); and EF will make this association for you

Erik提到您可以使用.AsNoTracking(),但我不确定他建议在什么时候使用它,但是如果您需要使用带有填充的导航属性的最终实体集(例如{{1 }}),您不能使用.AsNoTracking(),这会使EF缓存中的实体之间的关联无效(再次,来自MSFT文档的8.2.2):

  

此[分解EF查询]仅适用于跟踪查询,因为我们正在利用上下文必须自动执行身份解析和关联修正的功能。

为提高性能,如果您的查询是只读的,即您不更新值,则可以在query上设置以下属性(假设您急于加载所有必需的数据):< / p>

DbContext

最后,您的DbContext应该具有每个请求的生存期/范围。

肯(Ken)的观点是,如果您的数据库体系结构混乱不堪,运行探查器/查看执行计划可以帮助您调整索引/识别其他问题,但是在考虑打开探查器之前,您的查询就受到了限制{{1 }} Configuration.LazyLoadingEnabled = false; Configuration.AutoDetectChangesEnabled = false; Configuration.ProxyCreationEnabled = false; ,您应该会发现仅此一项就可以大大提高速度。