实体框架性能问题

18

我在使用 Entity Framework 和 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毫秒。
使用ANTS性能分析器发现大部分时间都花费在做一些不同的事情上:
Equals方法被调用了5000万次。
请问是否有人知道为什么需要调用这个方法5000万次以及如何提高性能?

你是否实际查看了语句生成的查询,还是仅仅假设它是最优化的查询? - Justin Niessner
1
问题并不在于我所说的查询。我使用 EF 生成的确切查询,并在使用常规 ADO.net 的 Sql 数据适配器中使用它,手动加载相同的对象。它在不到一秒钟的时间内运行。 - Dismissile
IL 长什么样? - rice
你的导航属性应该标记为 virtual。我不确定这是否相关。 - Yuck
1
@Yuck - 不,我不想要懒加载。我想急切地加载所有的10,000条记录,以便确定我们是否可以在我们的应用程序中使用它。我们将有需要加载非常大的对象图的情况。我不认为将其设置为懒加载或急切加载会改善这种情况。 - Dismissile
显示剩余4条评论
2个回答

20

为什么Equals方法被调用了5000万次?

这听起来相当可疑。你有1万个评论,却调用了5000万次Equals方法。假设这是由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方法的对象类型吗?我认为它应该只比较主键,即使有5000万个整数比较也不应该是问题。

顺便说一下,EF很慢 - 这是众所周知的事实。它在实例化实体时还会内部使用反射,因此仅仅10000条记录可能就需要"一些时间"。除非你已经这样做了,否则你还可以关闭动态代理创建 (db.Configuration.ProxyCreationEnabled)。


厉害的分析!根据我之前进行的测试(简单实体,没有导航属性),使用AsNoTracking可以将实体物化时间减少50%。不过,我可以想象,对于作为跟踪加载的实体而言,快照创建的成本可能比在标识映射中调用Equals更高。如果您在同一上下文中多次调用相同查询(都是跟踪加载)会非常迅速(第一次调用的1/10以下),远比不跟踪加载快 - 这让我猜测,标识映射中的Equals检查是相对便宜的。 - Slauma
顺便提一下:Include 也可以与 AsNoTracking() 一起使用,导航集合会被填充。(或者你是指反向导航属性 Review.Book 不会被填充吗?) - Slauma

1

我知道这听起来很无聊,但你尝试过反过来做吗?例如:

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

我注意到当你用这种方式处理查询时,EF有时会表现得更好(但我还没有时间弄清楚为什么)。


我个人会避免这样做,因为会出现死锁问题。 - Skarsnik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接