18

我遇到了一个有趣的实体框架性能问题。我使用Code First。实体框架性能问题

这里是我的实体结构:

一本书可以有很多评论。 评论与单一书相关联。 评论可以有一个或多个评论。 评论与一篇评论相关联。

public class Book 
{ 
    public int BookId { get; set; } 
    // ... 
    public ICollection<Review> Reviews { get; set; } 
} 

public class Review 
{ 
    public int ReviewId { get; set; } 
    public int BookId { get; set; } 
    public Book Book { get; set; } 
    public ICollection<Comment> Comments { get; set; } 
} 

public class Comment 
{ 
    public int CommentId { get; set; } 
    public int ReviewId { get; set; } 
    public Review Review { get; set; } 
} 

我使用大量数据填充我的数据库并添加了正确的索引。我正尝试使用此查询检索单个有10,000条评论的图书:

var bookAndReviews = db.Books.Where(b => b.BookId == id) 
         .Include(b => b.Reviews) 
         .FirstOrDefault(); 

这本书有10,000条评论。这个查询的性能大约为4秒。运行完全相同的查询(通过SQL Profiler)实际上很快就会返回。我使用相同的查询和一个SqlDataAdapter和自定义对象来检索数据,它发生在500毫秒以下。

使用蚂蚁性能分析器,它看起来像一大块的时间被消耗在做一些不同的事情:

equals方法被调用5000万次。

有谁知道为什么它需要调用这5000万次,我怎样才能提高性能呢?

+0

您是否确实看到您的语句正在生成哪个查询,或者您认为它是最佳查询? –

+1

试试EF Profiler。 –

+1

问题不在于我说过的查询。我使用EF生成的确切查询,并使用常规ADO.net在Sql Data Adapter中使用它,手动加载相同的对象。它运行时间不到一秒钟。 – Dismissile

回答

20

为什么Equals被称为50M倍?

听起来颇为可疑。您可以拨打Equals拨打10.000条评论和拨打50.000.000个电话。假设这是由EF内部实现的身份映射引起的。标识映射确保每个具有唯一键的实体仅由上下文跟踪一次,因此如果上下文已经具有与数据库中加载的记录相同的键的实例,则不会实现新实例,而是使用现有的实例。现在如何能与这些数字重合?我的恐怖猜测:

============================================= 
1st  record read | 0  comparisons 
2nd  record read | 1  comparison 
3rd  record read | 2  comparisons 
... 
10.000th record read | 9.999 comparisons 

这意味着每个新记录都与身份图中的每个现有记录进行比较。通过应用数学,计算所有的比较,我们可以用一种叫“等差数列”的总和:

a(n) = a(n-1) + 1 
Sum(n) = (n/2) * (a(1) + a(n)) 
Sum(10.000) = 5.000 * (0 + 9.999) => 5.000 * 10.000 = 50.000.000 

我希望我没有让错误在我的假设或计算。等待!我希望我错了,因为这看起来不太好。

尝试关闭更改跟踪=有希望关闭身份映射检查。

这可能会很棘手。首先:

var bookAndReviews = db.Books.Where(b => b.BookId == id) 
          .Include(b => b.Reviews) 
          .AsNoTracking() 
          .FirstOrDefault(); 

但是有一个很大的机会,你的导航属性将不会填充(因为它是由更改跟踪处理)。在这种情况下使用这种方法:

var book = db.Books.Where(b => b.BookId == id).AsNoTracking().FirstOrDefault(); 
book.Reviews = db.Reviews.Where(r => r.BookId == id).AsNoTracking().ToList(); 

无论如何,你能看到什么对象类型传递给Equals?我认为它应该只比较主键,甚至50M的整数比较不应该是这样的问题。

作为一个附注EF很慢 - 这是众所周知的事实。它也在实体化时使用内部反射,因此简单的10.000条记录可能需要“一段时间”。除非您已经这样做了,否则您也可以关闭动态代理创建(db.Configuration.ProxyCreationEnabled)。

+0

真棒分析!根据测试(简单的没有导航属性的实体),我在一段时间之前已经提出,'AsNoTracking'将实现时间缩短到50%。我可以想象,虽然为被追踪的实体创建快照比在标识映射中调用“Equals”更昂贵。如果你在相同的上下文中第二次调用相同的查询(都被跟踪),它会快速返回(小于第一次调用的1/10),比没有跟踪的加载快得多 - 这让我猜测“Equals”检查在身份地图中相对便宜。 – Slauma

+0

顺便提一下:'Include'也可以和'AsNoTracking()'一起使用,导航集合会被填充。 (或者你的意思是反向导航属性'Review.Book'不会被填充?) – Slauma

1

我知道这听起来有点扯,但你有没有尝试过周围的其他方式,如:

var reviewsAndBooks = db.Reviews.Where(r => r.Book.BookId == id) 
         .Include(r => r.Book); 

当你接近你的查询这样我已经注意到从EF有时更好的性能(但我没有找出原因的时间)。

+0

我个人会避免这个问题与僵局。 – Skarsnik