使用LINQ处理大型数据集

6
每次我使用LINQ to SQL编写下面这种形式的程序时,都会得到一个随着运行而不断占用更多内存并在处理大约25,000条记录后崩溃的程序,最终我总是不得不使用ADO.NET重写它。我做错了什么?澄清:这个问题不是关于处理速度的,关于加速的答案与此无关。
foreach (int i=0; i<some_big_number; i++)
{
    using (myDC dc = new myDC())  // my DataContext
    {
        myRecord record = (from r in dc.myTable where r.Code == i select r).Single();

        // do some LINQ queries using various tables from the data context
        // and the fields from this 'record'.  i carefully avoid referencing
        // any other data context than 'dc' in here because I want any cached
        // records to get disposed of when 'dc' gets disposed at the end of 
        // each iteration.

        record.someField = newValueJustCalculatedAbove;
        dc.SubmitChanges();
    }
}

3
我认为“What am I doing wrong?”的答案是“重复同样的事情,却期望获得不同的结果。”这也是疯狂的表现。为了好玩,请尝试其他方法。例如,首先使用ADO.Net编写您的SQL访问代码,跳过所有那些linq垃圾。 - NotMe
4
但正如你所暗示的那样,LINQ 并非渣滓。它是最优雅的技术之一,坚持使用它证明了我的立场,而不是疯狂的表现。 - Nestor
4个回答

6
你正在给数据上下文施加压力,每次都从头开始生成查询。尝试使用已编译的查询。

转向编译查询确实有很大的改善,将内存消耗降低了至少95%。我猜不断重新编译查询本来就是个坏主意,但我仍然好奇为什么它会使用那么多内存。为了让其他人受益,这里提供一个入门指南:http://linqinaction.net/blogs/jwooley/archive/2007/09/04/linq-to-sql-compiled-queries.aspx - Nestor
编译查询并不能完全替代非编译查询,您需要对代码进行一些更改。这篇文章帮助我理解了其中的原因:http://linqinaction.net/blogs/jwooley/archive/2007/09/04/linq-to-sql-compiled-queries.aspx - Nestor
2
7年后,这证明是修复遗留应用程序中性能差的一个救星。非常感谢。 - Rob G
@RobG 很高兴能帮到你。 - leppie

3
每次循环迭代都要两次访问数据库——一次检索行,一次更新行。这不是很有效率。
你应该分批操作:
- 通过选择范围而不是单个值来提前获取一组行,例如第一批0-100,下一批101-200等。如果Code列上定义了聚集索引,则这将是最快的。 - 在进入循环之前创建数据上下文 - 在循环内部,只需更新对象 - 循环结束后调用SubmitChanges(),这将在单个连接/事务中将所有更新发送到数据库 - 重复进行下一批
您应该使批处理大小可配置,因为您无法确定哪个批处理大小会产生最佳性能——不要将其硬编码到应用程序中。
此外,我建议使用SingleOrDefault()和null检查,而不是Single(),除非您可以保证对于任何i的值都会有一行。
编辑:
就记忆使用而言,这更难控制,但它并不特定于LINQ to SQL,任何批处理算法都必须处理这个问题。虽然我不建议在实践中使用GC.Collect(),但在处理大批量之后,通常作为解决方法足够了。
您还可以尝试减少每行检索的数据量(具体取决于初始值),可以创建一个新实体,将其映射到同一表中的一组更小的列,可能只有一两个,这样当您选择该实体时,您只检索了您打算一开始就使用的列。这将提高速度和内存占用量,因为传输的数据量较少,对象也更小。

当然,这些都是不错的速度优化方法,但应该在程序正常运行后进行优化。目前可用内存很快就会被占满,导致程序无法继续执行。 - Nestor
循环结束后调用SubmitChanges()对内存利用会有什么影响?我们这里不讨论速度。 - Nestor
@Sam,我认为你还是有些误解。你提到的内存使用想法可以减少每次迭代使用的内存,但实际上会在连续迭代中导致内存使用量的大量和快速积累。减少每行数据的数量并不能解决这个问题。我看到的问题确实是Linq to SQL特有的,因为不再需要的实体在迭代之间没有被释放。 - Nestor
@maxc - 我现在能建议的就是启动一个分析器,看看内存消耗/保留在哪里。 - Sam

1

我无法复制这个问题。内存使用率保持不变。虽然性能较慢,但内存一直稳定。

你确定没有其他地方泄漏吗?你能提供一个最小的代码示例来重现这个问题吗?

编辑:

我使用了几乎相同的示例代码:

for (int ii = 1; ii < 200000; ii++)
{
    using (var dc = new PlayDataContext())
    {
        var record = 
            (from r in dc.T1s where r.Id == ii select r).SingleOrDefault();
        if (record != null)
        {
            record.Name = "S";
            dc.SubmitChanges();
        }
    }
}

没有问题。

需要排除以下可能性:

  • 框架版本。我正在使用最新版。
  • DataContext/实体的复杂程度。我的测试表只有两个字段:Id(整数)和Name(nvarchar(max))。

您能否使用最新的框架和一个小的DataContext示例重现该问题?


我同意,我认为我们需要看更多你的代码来诊断问题。我也在像这样的大循环中使用LTS,但我还没有遇到你提到的“内存泄漏”问题... - Funka
这里有一个完整的示例供您参考,当然我无法包含数据上下文本身。在我的机器上,它每秒消耗超过1MB,并且在我编写此文时使用了720MB。for (int run = 0; run < 1000; run++) for (int i = 0; i < 50000; i++) { using (MyDC myDC = new MyDC()) { tblRecords record = (from r in myDC.tblRecords where r.Code == i select r).SingleOrDefault(); } } - Nestor

0

你可以尝试将数据上下文的创建放在for循环之外,不确定这样做能节省多少内存。

也许你可以在每个循环后调用GC.Collect(),看看是否可以手动触发垃圾回收。


1
我认为如果有什么东西会让情况变得更糟,那就是数据上下文然后存在足够长的时间来积累成千上万的缓存记录。但是显然我对某些事情存在误解,因为仍然有一些东西在不断地积累。 - Nestor
@maxc - 你是否尝试将数据上下文调用移出循环,以查看是否有任何区别? - Breadtruck
我已经抓狂了,是的。在这个问题上我已经折磨自己一年了。 - Nestor

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