Linq System.OutofMemoryException

5

我有一个长时间运行的C#进程,会从Sql表中查询10到200次。当该进程超过50次查询并且每次查询相同的表时,如果查询结果超过100,000行,它将在此行抛出System Out of Memory异常,具体地在将IQuery对象转换为List的底部:

var cht = from p in _db.TickerData
          where p.Time >= Convert.ToDateTime(start) &&
          p.Time <= Convert.ToDateTime(end)
          orderby p.Time
          select p;

_prices = cht.ToList();    < this is where the System.OutofMemoryException occurs >

我该如何避免这个错误?

我尝试在Sqlexpress上的[Time]列上创建索引。当前的索引是主键[Id]列。 - CraigJSte
当你尝试将10万行加载到内存中时,它会耗尽内存?你认为该如何解决这个问题? - DavidG
问题是,你在做 _prices 方面的什么操作?在了解之后,我们可以告诉你如何解决这个问题。 - Magnus
5个回答

6

首先:

特别是在将IQuery对象转换为List的底部

是的,您可以预期内存不足条件会发生在那里。

上面的cht赋值实际上并没有触及数据库;它只声明了查询的形状。这被称为延迟执行,LINQ无处不用。它的意思是“我们直到您的代码需要它之前都不会实际处理任何东西。”

调用ToList本质上意味着“代码现在需要全部内容”。因此,它会将查询发送到数据库,一次性提取所有结果,使用LINQ魔法将它们转换为CLR对象,并将它们全部放入List<T>中。

话虽如此,这只是一个预感,但您的LINQ提供程序可能不知道Convert.ToDateTime是什么。如果它不知道如何处理它,它就不会将其放入WHERE子句中执行的查询中,并且它将客户端加载整个表并进行过滤,这可能是当表变得太大而不是结果集变得太大时崩溃的原因。

为了验证这一点,请使用数据库分析器拦截查询,并查看WHERE子句是否符合您的预期。如果它没有正确转换,请尝试使用以下方法代替:

var startTime = Convert.ToDateTime(start);
var endTime = Convert.ToDateTime(end);
var cht = from p in _db.TickerData
          where p.Time >= startTime && p.Time <= endTime
          orderby p.Time
          select p;
_prices = cht.ToList();

如果这不起作用,那么很可能是您只是拉取了太多的数据,您需要像在任何其他情况下处理过多数据时一样处理它。


1
问题是由于 Convert.ToDateTime() 函数引起的。感谢 @Mason。 - CraigJSte
然而,那个问题只是真正问题的一个子集,真正的问题是使用ToList而不是使用IEnumerable来创建一个类对象,这似乎解决了更大的问题。希望这是对如何使用IEnumerable的准确描述。这个答案在评论中被@IVan提到过。 - CraigJSte

5
您试图检索的数据对于您的列表来说太大了。异常出现在ToList()上,因为这正是查询执行的地方。 您想通过这样一个大列表实现什么目标?可能的解决方案如下: 1)使用更多的条件限制您的搜索。不要加载整个数据,而是加载部分数据,如果确实需要,则加载另一部分数据。 2)如果您想将整个数据加载到内存中,请使用另一种数据结构,例如ConcurrentDictionary

4
你的问题在于查询返回了一个非常大的数据集,需要存储在我们进程的内存中。数据量太大会导致OutOfMemoryException异常。这是正常现象。不正常的是试图做这样的事情。相反,你可以使用一些额外的过滤条件来限制结果集,或将大的结果集分成较小的几个部分,例如:
        DateTime startDateTime = Convert.ToDateTime(start);
        DateTime endDateTime = Convert.ToDateTime(end);
        int fetched = 0;
        int totalFetched = 0;

        do
        {
            //fetch a batch of 1000 records
            var _prices = _db.TickerData.Where(p => p.Time >= startDateTime && p.Time <= endDateTime)
                                    .OrderBy(p => p.Time)
                                    .Skip(totalFetched)
                                    .Take(1000)
                                    .ToList();                

            //do here whatever you want with your batch of 1000 records
            fetched = _prices.Count;
            totalFetched += fetched;
        }
        while (fetched > 0);

这样你就可以分批处理任意数量的数据。
编辑:根据@Code.me在评论部分报告的问题进行了修复。
编辑:如果您还没有在数据库级别上为时间列设置索引,建议您这样做以加速这些查询。

我在这里看到三个问题。首先,Take(1000) 总是会获取前 1000 条记录。你需要在第一次迭代后使用 Skip(1000) 并继续获取。其次,在 while 语句中 _prices 超出了作用域。第三,IEnumerable<T> 没有 Count 属性。你需要在其上调用 .Count() 扩展方法。 - Code.me
@Code.me 感谢您发现这些问题。已经修复了前两个问题(当时匆忙发布了帖子...)。至于第三个问题,ToList() 返回一个具有 Count 属性的 List<T>。 - Mihai Caracostea
你是正确的。我总是使用IEnumerable<T>,忘记了它返回IList<T>。 - Code.me
@Code.me 公共静态方法 List<TSource> ToList<TSource>(this IEnumerable<TSource> source)。它确实返回一个 List<T>,而不是 IList<T>。 - Mihai Caracostea

2

由于延迟执行,查询将在调用ToList()时执行。由于加载所有数据会消耗太多内存,因此最好进行批处理。

以下代码将让您一次获取1000条记录(或任何适合您的数量),然后您可以对它们进行处理。

var startTime = Convert.ToDateTime(start);
var endTime = Convert.ToDateTime(end);

IEnumerable<T> prices= new List<T>();  // whatever T is

var currentFetched = 0;
var totalFetched = 0;

do
{
    var cht = _db.TickerData.Where(p => p.Time >= startTime && p.Time << endTime)
                        .OrderBy(p => p.Time)
                        .Skip(totalFetched)
                        .Take(1000)
                        .ToList();

    currentFetched = cht.Count();
    totalFetched += currentFetched;

    // prices = prices.Concat(cht).ToList();  
    // ^ This might throw an exception later when the list becomes too big 
    // So you can probably process currently fetched data
}
while (currentFetched > 0);

该实现检索记录1-1000,然后是1001-2000,以此类推重复相同的1001-2000,因为在第一次获取之后,您将始终仅跳过前1000条记录。 - Mihai Caracostea
啊,谢谢你注意到了。我在修复之前的一个打字错误时不小心删除了计算总抓取量的部分。现在已经修复了。 - Code.me
我不认为这个答案有什么帮助。如果 OP 想要处理查询结果,他可以自己将其转换为 IEnumerable 而不是使用 ToList。而且,现在在内存中加载 100K 条记录并不是什么大问题。 - Ivan Stoev
@Ivan,我将尝试你关于IEnumerable的建议。 - CraigJSte
@Ivan,我真正遇到的问题是通过创建一个IEnumerable类来解决的,而不是使用ToList...但你没有给我答案,只是对别人的答案进行了评论,所以我无法将其标记为正确! - CraigJSte
@CraigJSte 不用谢,说实话我认为你知道,ToList通常用于需要绑定UI或对查询结果执行多个操作的情况。但是请注意不要使用Linq中的其他语法糖,例如Any,Count等,因为IEnumerable不会缓存查询结果,并且会重新执行查询。无论如何,很高兴你的问题已经解决,这是最重要的,你选择的答案也很好。保重。 - Ivan Stoev

0
我对我的做法是直接返回一个IQueryable对象。它仍然是一个列表,但性能要好得多。

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