聚合/过滤导航属性时的性能命中/内存消耗

时间:2015-02-08 14:03:38

标签: c# entity-framework lazy-loading entity-framework-6 navigation-properties

我们说我有以下几类:

public class MegaBookCorporation
{
    public int ID { get; private set}
    public int BooksInStock 
    {
        get
        {
            return Stores.Sum( x => x.BooksInStock)
        }
    }
    public virtual ICollection<MegaBookCorporationStore> Stores { get; set; }
}


public class MegaBookCorporationStore
{
    public int ID { get; private set; }
    public string BookStoreName { get; private get; }
    public virtual MegaBookCorporation ManagingCorporation { get; private set;}
    public int BooksInStock
    {
        get
        {
            return Books.Where( x=> !x.IsSold).Count();
        }
    }

    public virtual ICollection<Book> Books { get; set; }
}

public class Book
{
    public int IndividualBookTrackerID { get; private set; }
    public virtual MegaBookCorporationStore { get; private set; }
    public bool IsSold { get; private set; }
    public DateTime? SellingDate { get; private set;}
}

我在工作中讨论了在MegaBookCorporation中检索NumberOfBooks时遇到的性能问题。两个重要的事实:

1 /我们按照虚拟关键字的建议使用带延迟加载的EF 6。

2 /由于每本书都是单独跟踪的,因此数据库中的书籍条目数量将很快变得很快。从长远来看,该表的规模可能会达到数亿。我们每天可能会增加100,000本书。

我支持的意见是目前的实施情况很好,而且我们不会遇到问题。我的理解是,在调用GetEnumerator时,将生成一个SQL语句来过滤集合。

我的同事提出的另一个建议是缓存书籍数量。这意味着更新字段&#34; int ComputedNumberOfBooks&#34;每当调用AddBookToStock()或SellBook()方法时。需要在Store和Corporation类中重复和更新此字段。 (当然我们需要注意并发)

我知道添加这些字段并不是什么大问题,但我对这个想法感到非常不满。对我来说,它似乎预先设计了一个不存在的问题,并且在我看来并不存在。

我决定再次检查我的索赔,发现了两个相互矛盾的答案:

One saying that the whole Books collection would be pulled to memory,因为ICollection只继承自IEnumerable。 The other saying the opposite : the navigation property will be treated as an IQueryable until it is evaluated。(为什么不,因为该属性由代理包装)

所以这是我的问题:

1-什么是真相?

2-即使引用了整个集合,也不要认为它不是什么大问题,因为它是IEnumerable(内存使用率低)。

3-您如何看待此示例中的内存消耗/性能,以及最佳方法是什么?

谢谢

2 个答案:

答案 0 :(得分:1)

  

真相是什么?

如果您使用MegaBookCorporation.BooksInStock来获取存储的图书总数,则将从数据库中加载所有图书。除了获取所有数据并在内存中进行评估之外,查询提供程序无法为属性getter的主体生成SQL表达式。

  

即使引用了整个集合,也不要认为它不是什么大问题,因为它是IEnumerable(内存使用率低)。

是的,这是一个大问题,因为它根本没有扩展。它与IEnumerable的事实无关。问题是在评估Count()之前获取所有数据

  

您如何看待此示例中的内存消耗/性能,以及最佳方法是什么?

内存消耗将随着数据库中存储的书籍数量而增长。既然你只想得到他们的数量,那显然是不行的。 Here你可以看到如何正确地做到这一点。

答案 1 :(得分:0)

判决

事实是,通过您定义的属性,可以加载整个书籍集。这就是原因。

理想情况下,您希望能够

var numberOfBooks = context.MegaBookCorporations
                           .Where(m => m.ID == someId)
                           .Select(m => m.BooksInStock)
                           .Single();

如果EF能够将其转换为SQL,那么您的查询只返回一个整数并且不会将任何实体加载到内存中。

但不幸的是,EF无法做到这一点。它将抛出BooksInStock没有SQL转换的异常。

要绕过这个例外,你可以做:

var numberOfBooks = context.MegaBookCorporations
                           .Where(m => m.ID == someId)
                           .Single()
                           .BooksInStock;

这极大地改变了事情。 Single()将一个MegaBookCorporation吸引到内存中。访问其BooksInStock属性会触发延迟加载MegaBookCorporation.Stores。随后,对于每个Store,将加载完整的Books个集合。最后,LINQ操作(x => !x.IsSoldCountSum)将应用于内存中。

所以在这种情况下,the first link是正确的。延迟加载总是加载完整的集合。加载集合后,将不会再次加载它们。

second link也是正确的:)。

只要您设法在一个可以转换为SQL的LINQ语句中执行所有操作,就会在数据库中评估导航属性和谓词,并且不会发生延迟加载。但是,您无法使用BooksInStock属性。

实现这一目标的唯一方法是使用像

这样的LINQ语句
var numberOfBooks = context.MegaBookCorporations
                           .Where(m => m.ID == someId)
                           .SelectMany(m => m.Stores)
                           .SelectMany(s => s.Books)
                           .Count();

这使用一个连接和COUNT执行非常有效的查询,只返回计数。

不幸的是,你的关键假设......

  

在调用GetEnumerator时,将生成一个SQL语句来过滤集合。

不完全正确。生成SQL语句,但不包括过滤器。你提到的书籍数量会导致严重的性能和内存问题。

那该怎么办?

如果您经常需要这些计数,并且您不想一直单独查询它们,那么应该做些什么。您的同事的想法,数据库中的冗余ComputedNumberOfBooks字段可能是一个解决方案,但我同意您的观点。

应该以(几乎)所有成本避免冗余。最糟糕的是,它始终需要客户端应用程序来保持双方同步。或数据库触发器。

但是谈论数据库......如果这些计数很重要且经常被查询,我会在BooksInStock表中引入一个计算列MegaBookCorporationStore。它的公式可以简单地计算商店中的书籍数量。然后,您可以将此计算列作为标记为DatabaseGeneratedOption.Computed的属性添加到您的实体。没有冗余。