使用IDataReader时,C#抛出了“System.OutOfMemoryException”类型的异常。

11

我有一个应用程序,需要从数据库中获取大量数据。由于无法获取所有行(接近200万行...),我将其分成几个部分,每次运行SQL查询并仅获取每次200,000行。

我使用DataTable来输入所有数据(即所有200万行都应该在其中)。

前几次运行正常。然后出现OutOfMemoryException错误。

我的代码工作方式如下:

private static void RunQueryAndAddToDT(string sql, string lastRowID, SqlConnection conn, DataTable dt, int prevRowCount)
    {
        if (string.IsNullOrEmpty(sql))
        {
            sql = generateSqlQuery(lastRowID);
        }

        if (conn.State == ConnectionState.Closed)
        {
            conn.Open();
        }

        using (IDbCommand cmd2 = conn.CreateCommand())
        {
            cmd2.CommandType = CommandType.Text;
            cmd2.CommandText = sql;
            cmd2.CommandTimeout = 0;

            using (IDataReader reader = cmd2.ExecuteReader())
            {
                while (reader.Read())
                {
                    DataRow row = dt.NewRow();
                    row["RowID"] = reader["RowID"].ToString();
                    row["MyCol"] = reader["MyCol"].ToString();
                    ... //In one of these rows it returns the exception.

                    dt.Rows.Add(row);
                }
            }
        }

        if (conn != null)
        {
            conn.Close();
        }

        if (dt.Rows.Count > prevRowCount)
        {
            lastRowID = dt.Rows[dt.Rows.Count - 1]["RowID"].ToString();
            sql = string.Empty;
            RunQueryAndAddToDT(sql, lastRowID, conn, dt, dt.Rows.Count);
        }
    }

读者似乎一直在收集行,这就是为什么只在第二或第三轮才会引发异常。

难道Using不应该清理内存吗?有什么方法可以解决我的问题吗?

注意:我必须解释-除了将所有行转换为datatable之外,我别无选择,因为我稍后要对它们进行一些操作,而行的顺序很重要,我不能拆分它,因为有时我必须获取某些行的数据并将其设置为一行等等,所以我不能放弃。

谢谢。


1
只是一个评论,如果数据增加一倍或三倍,会怎样呢?必须有一种方法只拉回部分数据,否则你以后可能会遇到很多麻烦。 - Ross Dargan
加载2M行数据是相当不常见的 - 大多数情况下的想法是尽量减少加载的数据量。如果您需要2M行数据,DataTable可能不是最佳模型(DataTable具有开销)。我个人会将其加载到POCO模型中。 - Marc Gravell
4个回答

18

确保您正在构建64位进程,而不是32位进程,因为Visual Studio的默认编译模式是32位。要做到这一点,请右键单击项目,属性-> 构建->平台目标:x64。与任何32位进程一样,以32位编译的Visual Studio应用程序具有2GB的虚拟内存限制。

64位进程没有此限制,因为它们使用64位指针,所以它们的理论最大地址空间为16 exabytes(2 ^ 64)。实际上,Windows x64将进程的虚拟内存限制为8TB。解决内存限制问题的方法是编译成64位。

但是,默认情况下,Visual Studio中对象的大小仍然受到2GB的限制。您将能够创建多个数组,其组合大小将大于2GB,但默认情况下无法创建大于2GB的数组。如果您仍想创建大于2GB的数组,则可以通过在app.config文件中添加以下代码来实现:

<configuration>
  <runtime>
    <gcAllowVeryLargeObjects enabled="true" />
  </runtime>
</configuration>

对我有用!谢谢! - Daniel Gee
谢谢。对我有用。 - Natiq
我正在使用32位应用程序,尝试从SQL Reader中添加1700000条记录到数据表中,但出现了内存不足异常。使用上述解决方案后,再次出现了相同的错误。请建议我如何解决这个问题。 - Anand

4
我认为你的DataTable因为添加了太多行而导致内存不足。
在这种情况下,您可以尝试使用不同的模式。
不要将行缓冲到列表(或DataTable)中,而是在可用时立即生成行。

3

因为您正在使用DataTable,所以让我分享一个我使用它时遇到的随机问题。请检查您的构建属性。我曾经遇到过DataTable随机抛出内存不足异常的问题。事实证明,这是由于项目的构建平台目标设置为Prefer 32-bit导致的。一旦取消选中该选项,随机的内存不足异常就消失了。


2
您正在将数据的副本存储到dt中。您只是存储了太多的数据,导致机器的内存不足。因此,您有几个选择:
  • 增加可用内存。
  • 减少检索的数据量。
要增加可用内存,您可以向计算机添加物理内存。请注意,32位计算机上的.NET进程将无法访问超过2GB的内存(如果您在boot.ini中启用3GB开关,则为3GB),因此,如果您希望访问更多的内存,可能需要切换到64位(计算机和进程)。
检索较少的数据可能是最好的方法。根据您想要实现的目标,您可以在数据的子集上执行任务(甚至在单个行上执行)。如果您正在执行某种聚合(例如从数据生成摘要或报告),则可以使用Map-Reduce

使用“using”并不能真正清除内存 - 它不会触发垃圾回收。此外:读取器只有一个小缓冲区。 - Marc Gravell
@MarcGravell:说得好,但这会使读者使用的内存有资格进行收集,如果结果没有被存储在其他地方,这将防止OOM。 - Paul Ruane
不行,因为读者并没有持有所有的数据:它是一个流式API。 - Marc Gravell

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