以前投稿したこちらの記事では、SqlBulkCopyを使用することによって、数十万件のデータを短時間で一括insertする方法を紹介した。
maitakeramen.hatenablog.com
しかし、例えば数十万行とある郵便番号のデータなどをinsertしようとした場合、
約100MBほどのメモリを占有することになった。(計測した。)
これは一度に大量のデータをメモリに格納する必要があるためだ。
これを回避するための方法を紹介する。
今回、例としてやってみる流れ
テキストファイルを読み込むための列挙子オブジェクト(Enumerable型オブジェクト)を取得
Enumerable型は、一度に全てのデータを読み込まず、1行ずつ反復処理することをサポートする。
↓
当記事では前準備として、EnumerableクラスをIDataReaderを使える拡張メソッドを実装する。
この拡張メソッドの返り値を引数とし、SqlBulkCopyする。
IDataReaderインターフェースは順方向専用ストリームを読み取る手段を提供する。
前準備
IDataReaderインターフェースを利用したEnumerableクラスの拡張メソッドを作成する。
using System; using System.Data; using System.Collections.Generic; using System.Reflection; namespace BulkTest { ////// Enumerable型の拡張クラス /// static class EnumerableExtention { ////// IDataReader型に変換して取得 /// ///変換元の型 /// 変換元 ///IDataReader型に変換されたオブジェクト public static IDataReader AsDataReader(this IEnumerable source) { return new EnumerableDataReader (source); } } /// /// Enumerable型のオブジェクトにIDataReaderの機能を実装 /// ///class EnumerableDataReader : IDataReader { /// IDataReaderの機能を実装するオブジェクト private readonly IEnumeratorm_source; /// プロパティ名の一覧 private readonly Listm_listProp = new List (); /// /// コンストラクタ /// /// IDataReaderの機能を実装するオブジェクト internal EnumerableDataReader(IEnumerablesource) { m_source = source.GetEnumerator(); m_listProp.AddRange(typeof(TSource).GetProperties()); } /// /// 要素を取得し、ポインタを次に進める /// ///public bool Read() { return m_source.MoveNext(); } /// /// フィールド数を取得 /// public int FieldCount { get { return m_listProp.Count; } } ////// 値を取得する /// /// ///public object GetValue(int i) { return m_listProp[i].GetValue(m_source.Current, null); } /* 以下、IDataReaderで必要なメソッドを実装する、こんな感じで */ /// /// IDataReaderの実装、例外を返すだけ /// public int Depth { get { throw new NotImplementedException(); } } } }
上記で作成した拡張メソッドAsDataReaderを利用し、SqlBulkCopy
using System; using System.Data; using System.Data.SqlClient; using System.Collections.Generic; using System.Reflection; using System.IO; using System.Linq; namespace BlogTest { class Program { static void Main(string[] args) { var lines = File.ReadLines("data.txt"); // Linqで反復して読み込むデータの構造を定義する。 var insertData = lines.Select(s => { // テーブルと同じ構造のモデルにデータを詰め込む(テーブルのカラムとプロパティは順番が同じであること) var columns = s.Split(','); var tableModel = new TableModel { Id = columns[0], ColumnA = columns[1], ColumnB = columns[2] + "接尾辞" // 無加工じゃ味気ないので }; return tableModel; }); var connectionString = "接続文字列"; using (var bulkCopy = new SqlBulkCopy(connectionString)) { bulkCopy.DestinationTableName = "テーブル名"; // テーブル名をSqlBulkCopyに教える bulkCopy.WriteToServer(insertData.AsDataReader()); // bulkCopy実行 } } } ////// テーブルのモデルクラス /// class TableModel { public string Id { get; set; } public string ColumnA { get; set; } public string ColumnB { get; set; } } }
この方法ならば、データを1行ずつ読み込むので、1行分のメモリしか消費しない。
データの加工はLinqの式の中で自由にできる。
注意点
上述した方法は、行単位でデータを加工することはできるが、行をまたいだ処理はできない。
例えば、distinctで重複削除するなどということをすると、全てのデータを読み込む必要があるのでその時点でメモリを消費してしまう。
グルーピングなどしても同じである。
本末転倒ではあるが、メモリを気にしないならなんでもできる。
まぁ、モデルクラスを使って出来るので、メモリ云々を抜きにしてもコードがスッキリするかもしれない。