4

StreamReader を介して 800MB のテキスト ファイルを DataTable にロードしようとしているときに、OutOfMemory 例外が発生しました。バッチでメモリ ストリームから DataTable をロードする方法があるかどうか疑問に思っていました。すぐ。

ここでは私のグーグルはあまり役に立ちませんでしたが、これを行う簡単な方法があるはずです。最終的には、SqlBulkCopy を使用して DataTables を MS SQL db に書き込むことになるので、説明した方法よりも簡単な方法がある場合は、正しい方向への迅速なポインターに感謝します。

編集 - これが私が実行しているコードです:

public static DataTable PopulateDataTableFromText(DataTable dt, string txtSource)
{

    StreamReader sr = new StreamReader(txtSource);
    DataRow dr;
    int dtCount = dt.Columns.Count;
    string input;
    int i = 0;

    while ((input = sr.ReadLine()) != null)
    {

        try
        {
            string[] stringRows = input.Split(new char[] { '\t' });
            dr = dt.NewRow();
            for (int a = 0; a < dtCount; a++)
            {
                string dataType = dt.Columns[a].DataType.ToString();
                if (stringRows[a] == "" && (dataType == "System.Int32" || dataType == "System.Int64"))
                {
                    stringRows[a] = "0";
                }
                dr[a] = Convert.ChangeType(stringRows[a], dt.Columns[a].DataType);

            }
            dt.Rows.Add(dr);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
        i++;
    }
    return dt;
}

そして、返されるエラーは次のとおりです。

「System.OutOfMemoryException: 'System.OutOfMemoryException' 型の例外がスローされまし
た。System.String.Split(Char[] セパレーター、Int32 カウント、StringSplitOptions オプション)
で System.String.Split(Char[] セパレーター}
で Harvester.Config .PopulateDataTableFromText(DataTable dt, String txtSource) in C:...."

データを SQL に直接ロードするという提案について - 私は C# に関しては少し初心者ですが、基本的にはそれが私がやっていることだと思いましたか? SqlBulkCopy.WriteToServer は、テキスト ファイルから作成した DataTable を取得し、SQL にインポートします。私が見逃しているこれを行うためのさらに簡単な方法はありますか?

編集: 言い忘れましたが、このコードは SQL Server と同じサーバーでは実行されません。データ テキスト ファイルはサーバー B にあり、サーバー A のテーブルに書き込む必要があります。これにより、bcp を使用できなくなりますか?

4

4 に答える 4

5

データを SQL Server に直接ロードしてから、データベースで操作することを検討しましたか? データベース エンジンは、大量のデータを効率的に操作できるように設計されています。これにより、全体的により良い結果が得られ、データベースと SQL 言語の機能を活用して面倒な作業を行うことができます。それは古い「懸命に働くより賢く働け」という原則です。

SQL Server にデータをロードするにはさまざまな方法があるため、これらを調べて、適切なものがあるかどうかを確認することをお勧めします。SQLServer 2005 以降を使用していて、C# でデータを操作する必要がある場合は、いつでもマネージ ストアド プロシージャを使用できます。

ここで理解しておくべきことOutOfMemoryExceptionは、 が少し誤解を招くということです。メモリは、物理 RAM の量だけではありません。あなたが使い果たしている可能性が高いのは、アドレス可能なメモリです。これは非常に異なることです。

大きなファイルをメモリにロードして変換すると、同じデータを表すのに 800Mb 以上DataTableが必要になる可能性があります。32 ビット .NET プロセスは、アドレス指定可能なメモリが 2Gb 弱に制限されているため、この量のデータを 1 回のバッチで処理することはおそらく不可能です。

おそらく行う必要があるのは、ストリーミング方式でデータを処理することです。DataTableつまり、すべてを にロードしてから SQLServer に一括挿入しようとしないでください。むしろ、ファイルをチャンクで処理し、処理が完了したら前の行のセットをクリアします。

ここで、(VM のスラッシングを避けるため) 大量のメモリを備えた 64 ビット マシンと 64 ビット .NET ランタイムのコピーにアクセスできる場合は、コードを変更せずに実行するだけで済む可能性があります。ただし、その環境でもパフォーマンスが向上する可能性があるため、とにかく必要な変更を加えることをお勧めします。

于 2010-09-28T20:32:14.100 に答える
3

実際に行のバッチでデータを処理する必要がありますか? または、行ごとに処理できますか?後者の場合、Linq はメソッドの「パイプライン」を介してデータを簡単にストリーミングできるため、ここで非常に役立つと思います。そうすれば、一度に大量のデータをロードする必要がなく、一度に 1 行だけロードできます。

StreamReaderまず、列挙可能にする必要があります。これは、拡張メソッドを使用して簡単に実行できます。

public static class TextReaderExtensions
{
    public static IEnumerable<string> Lines(this TextReader reader)
    {
        string line;
        while((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }
}

そうすればStreamReader、Linq クエリのソースとして を使用できます。

次に、文字列を受け取り、それを に変換するメソッドが必要ですDataRow

DataRow ParseDataRow(string input)
{
    // Your parsing logic here
    ...
}

これらの要素を使用すると、ファイルの各行を DataRow に簡単に射影し、必要なことを行うことができます。

using (var reader = new StreamReader(fileName))
{
    var rows = reader.Lines().Select(ParseDataRow);
    foreach(DataRow row in rows)
    {
        // Do something with the DataRow
    }
}

(Linqを使用せずに単純なループで同様のことを行うことができますが、Linqを使用するとコードが読みやすくなると思います...)

于 2010-09-28T21:28:02.890 に答える
2

SqlBulkCopy.WriteToServer には、IDataReader を受け入れるオーバーロードがあります。Read() メソッドが StreamReader から 1 行を消費する StreamReader のラッパーとして、独自の IDataReader を実装できます。このようにして、データは、最初に DataTable としてメモリに構築しようとするのではなく、データベースに「ストリーミング」されます。それが役立つことを願っています。

于 2010-09-28T21:21:03.567 に答える
0

ここでの他の回答の更新として、私もこれを調査していて、このページに出くわしました。これは、テキストファイルをチャンクで読み取り、並行して処理し、データベースに一括挿入する優れた C# の例を提供します

コードの核心は、このループ内にあります。

//Of note: it's faster to read all the lines we are going to act on and 
            //then process them in parallel instead of reading and processing line by line.
            //Code source: http://cc.davelozinski.com/code/c-sharp-code/read-lines-in-batches-process-in-parallel
            while (blnFileHasMoreLines)
            {
                batchStartTime = DateTime.Now;  //Reset the timer

                //Read in all the lines up to the BatchCopy size or
                //until there's no more lines in the file
                while (intLineReadCounter < BatchSize && !tfp.EndOfData)
                {
                    CurrentLines[intLineReadCounter] = tfp.ReadFields();
                    intLineReadCounter += 1;
                    BatchCount += 1;
                    RecordCount += 1;
                }

                batchEndTime = DateTime.Now;    //record the end time of the current batch
                batchTimeSpan = batchEndTime - batchStartTime;  //get the timespan for stats

                //Now process each line in parallel.
                Parallel.For(0, intLineReadCounter, x =>
                //for (int x=0; x < intLineReadCounter; x++)    //Or the slower single threaded version for debugging
                {
                    List<object> values = null; //so each thread gets its own copy. 

                    if (tfp.TextFieldType == FieldType.Delimited)
                    {
                        if (CurrentLines[x].Length != CurrentRecords.Columns.Count)
                        {
                            //Do what you need to if the number of columns in the current line
                            //don't match the number of expected columns
                            return; //stop now and don't add this record to the current collection of valid records.
                        }

                        //Number of columns match so copy over the values into the datatable
                        //for later upload into a database
                        values = new List<object>(CurrentRecords.Columns.Count);
                        for (int i = 0; i < CurrentLines[x].Length; i++)
                            values.Add(CurrentLines[x][i].ToString());

                        //OR do your own custom processing here if not using a database.
                    }
                    else if (tfp.TextFieldType == FieldType.FixedWidth)
                    {
                        //Implement your own processing if the file columns are fixed width.
                    }

                    //Now lock the data table before saving the results so there's no thread bashing on the datatable
                    lock (oSyncLock)
                    {
                        CurrentRecords.LoadDataRow(values.ToArray(), true);
                    }

                    values.Clear();

                }
                ); //Parallel.For   

                //If you're not using a database, you obviously won't need this next piece of code.
                if (BatchCount >= BatchSize)
                {   //Do the SQL bulk copy and save the info into the database
                    sbc.BatchSize = CurrentRecords.Rows.Count;
                    sbc.WriteToServer(CurrentRecords);

                    BatchCount = 0;         //Reset these values
                    CurrentRecords.Clear(); //  "
                }

                if (CurrentLines[intLineReadCounter] == null)
                    blnFileHasMoreLines = false;    //we're all done, so signal while loop to stop

                intLineReadCounter = 0; //reset for next pass
                Array.Clear(CurrentLines, 0, CurrentLines.Length);

            } //while blnhasmorelines
于 2016-11-09T04:05:36.993 に答える