4

我觉得问这个问题很尴尬,因为我觉得我应该已经知道了。但是,鉴于我没有......我想知道如何将大文件从磁盘读取到数据库而不会出现 OutOfMemory 异常。具体来说,我需要加载 CSV(或真正的制表符分隔文件)。

我正在试验,CSVReader特别是这个代码示例,但我确定我做错了。他们的一些其他编码示例展示了如何读取任何大小的流文件,这几乎是我想要的(只需要从磁盘读取),但我不知道IDataReader我可以创建什么类型来允许这个。

我正在直接从磁盘读取,并且我试图通过一次读取太多数据来确保我永远不会耗尽内存。我不禁想到我应该能够使用 aBufferedFileReader或类似的东西,我可以指向文件的位置并指定缓冲区大小,然后CsvDataReader期望 aIDataReader作为它的第一个参数,它可以使用它。请告诉我我的方法的错误,让我摆脱我的GetData方法与它的任意文件分块机制,并帮助我解决这个基本问题。

    private void button3_Click(object sender, EventArgs e)
    {   
        totalNumberOfLinesInFile = GetNumberOfRecordsInFile();
        totalNumberOfLinesProcessed = 0; 

        while (totalNumberOfLinesProcessed < totalNumberOfLinesInFile)
        {
            TextReader tr = GetData();
            using (CsvDataReader csvData = new CsvDataReader(tr, '\t'))
            {
                csvData.Settings.HasHeaders = false;
                csvData.Settings.SkipEmptyRecords = true;
                csvData.Settings.TrimWhitespace = true;

                for (int i = 0; i < 30; i++) // known number of columns for testing purposes
                {
                    csvData.Columns.Add("varchar");
                }

                using (SqlBulkCopy bulkCopy = new SqlBulkCopy(@"Data Source=XPDEVVM\XPDEV;Initial Catalog=MyTest;Integrated Security=SSPI;"))
                {
                    bulkCopy.DestinationTableName = "work.test";

                    for (int i = 0; i < 30; i++)
                    {
                        bulkCopy.ColumnMappings.Add(i, i); // map First to first_name
                    }

                    bulkCopy.WriteToServer(csvData);

                }
            }
        }
    }

    private TextReader GetData()
    {
        StringBuilder result = new StringBuilder();
        int totalDataLines = 0;
        using (FileStream fs = new FileStream(pathToFile, FileMode.Open, System.IO.FileAccess.Read, FileShare.ReadWrite))
        {
            using (StreamReader sr = new StreamReader(fs))
            {
                string line = string.Empty;
                while ((line = sr.ReadLine()) != null)
                {
                    if (line.StartsWith("D\t"))
                    {
                        totalDataLines++;
                        if (totalDataLines < 100000) // Arbitrary method of restricting how much data is read at once.
                        {
                            result.AppendLine(line);
                        }
                    }
                }
            }
        }
        totalNumberOfLinesProcessed += totalDataLines;
        return new StringReader(result.ToString());
    }
4

6 回答 6

3

实际上,您的代码正在从文件中读取所有数据并保存到TextReader(内存中)。TextReader然后你从保存服务器读取数据。

如果数据太大,则数据大小会TextReader导致内存不足。请尝试这种方式。

1)从文件中读取数据(每一行)。

2)然后将每一行插入服务器。

内存不足的问题将得到解决,因为只有在处理内存中的每条记录。

伪代码

begin tran

While (data = FilerReader.ReadLine())
{
  insert into Table[col0,col1,etc] values (data[0], data[1], etc)
}

end tran
于 2012-01-31T23:01:46.017 回答
3

可能不是您正在寻找的答案,但这就是BULK INSERT的设计目的。

于 2012-02-02T18:08:45.310 回答
1

我只想添加使用 BufferedFileReader 和 readLine 方法,并以上述方式进行。

基本上了解这里的责任。

BufferedFileReader 是从文件中读取数据的类(buffe wise)也应该有一个 LineReader。CSVReader 是一个实用程序类,用于读取数据,假设其格式正确。

SQlBulkCopy 你无论如何都在使用。

第二种选择

您可以直接进入数据库的导入工具。如果文件格式正确,程序的重点就是这个。那也会更快。

于 2012-02-02T09:47:24.233 回答
1

我认为您可能对数据的大小有疑问。每次我遇到这个问题时,它都不是数据的大小,而是循环数据时创建的对象的数量。

查看您的 while 循环,在 button3_Click(object sender, EventArgs e) 方法中将记录添加到数据库中:

TextReader tr = GetData();
using (CsvDataReader csvData = new CsvDataReader(tr, '\t'))

在这里,您每次迭代都声明并实例化两个对象——这意味着对于您读取的每个文件块,您将实例化 200,000 个对象;垃圾收集器不会跟上。

为什么不在 while 循环之外声明对象呢?

TextReader tr = null;
CsvDataReader csvData = null;

这样,gc 将有一半的机会。您可以通过对 while 循环进行基准测试来证明差异,毫无疑问,在您创建了几千个对象之后,您会注意到性能大幅下降。

于 2012-02-02T19:53:37.473 回答
0

伪代码:

while (!EOF) {
   while (chosenRecords.size() < WRITE_BUFFER_LIST_SIZE) {
      MyRecord record = chooseOrSkipRecord(file.readln());
      if (record != null) {
         chosenRecords.add(record)
      }
   }  
   insertRecords(chosenRecords) // <== writes data and clears the list
}

WRITE_BUFFER_LIST_SIZE 只是您设置的常数......更大意味着更大的批次,更小意味着更小的批次。大小为 1 是 RBAR :)。

如果您的操作足够大以至于中途失败是一种现实的可能性,或者如果中途失败可能会花费一些人不小的金钱,那么您可能还想将到目前为止处理的记录总数写入第二个表文件(包括您跳过的文件)作为同一事务的一部分,以便您可以在部分完成时从中断的地方继续。

于 2012-02-03T04:44:33.027 回答
0

我建议读取一个块并将其插入数据库,而不是一一读取 csv 行并一一插入到数据库中。重复此过程,直到读取整个文件。

您可以在内存中缓冲,例如一次 1000 行 csv,然后将它们插入数据库中。

int MAX_BUFFERED=1000;
int counter=0;
List<List<String>> bufferedRows= new ...

while (scanner.hasNext()){
  List<String> rowEntries= getData(scanner.getLine())
  bufferedRows.add(rowEntries);

  if (counter==MAX_BUFFERED){
    //INSERT INTO DATABASE
    //append all contents to a string buffer and create your SQL INSERT statement
    bufferedRows.clearAll();//remove data so it could be GCed when GC kicks in
  }
}
于 2012-02-03T05:38:21.757 回答