## 追記
.NET 6 版を書きました
## 本文
注意)あまり真面目に測ってません
今日も色々DataTableからデータを抽出(検索)する方法を比べてみました。前回よりも調べる対象をひろげて、DataViewも使ったりしてみました。
では、さっくりとやってみましょう。
ダミーデータ作成メソッド
// ダミーデータの入ったDataTableを作るメソッド private static DataTable Create(int rowCount, int columnCount) { var result = new DataTable("DummyTable"); // ダミー列の作成 COLUMN_0 〜 COLUMN_columnCountまで作る foreach (var column in Enumerable.Range(0, columnCount)) { result.Columns.Add("COLUMN_" + column); } // ダミーデータの作成 DATA_乱数の形で作る var random = new Random(); foreach (var row in Enumerable.Range(0, rowCount)) { var addRow = result.NewRow(); foreach (var column in Enumerable.Range(0, columnCount)) { addRow[column] = string.Format("DATA_{0}", random.Next(100)); } result.Rows.Add(addRow); } return result; } // 引数で渡された処理にどれくらい時間がかかったか出力するメソッド private static void Watch(Action action) { var watch = Stopwatch.StartNew(); action(); Console.WriteLine("かかった時間: {0}ms", watch.ElapsedMilliseconds); }
とりあえず、このメソッドがあることを前提にプログラムを書いていきます。ではいってみましょう。
Selectメソッド
こいつは、かなり遅いです。でも、ちょっと工夫すると早くなったりする可愛い一面もあります。やってみましょう。
まず、定番の遅い奴です。前に書いた記事では、この使い方で紹介しました。
for (int i = 0; i < 5; i++) { // 50000行 10列 var table = Create(50000, 10); Watch(() => { var rows = table.Select("COLUMN_0 = 'DATA_10' OR COLUMN_1 like 'DATA_1%'", "COLUMN_0"); Console.WriteLine("{0}行見つかりました", rows.Length); }); }
単一列の検索じゃなくて複数列に対する検索と、ソートまでやってます。実行結果は以下のようになりました。
5921行見つかりました かかった時間: 259ms 5929行見つかりました かかった時間: 297ms 6056行見つかりました かかった時間: 293ms 6002行見つかりました かかった時間: 250ms 5882行見つかりました かかった時間: 255ms
250msくらいかかっています。5万件からこの条件で探すなら、結構いいかなって感じもします。
じゃぁ他の方法を見てみます。
インデックスを作ってSelect
Selectメソッドは、事前にDataView作ってインデックスちゃんと作っておくと早くなったりします。今回はCOLUMN_0とCOLUMN_1で検索、ソートしてるのでDataViewのSortプロパティでCOLUMN_0とCOLUMN_1を指定しておいてSelectを呼んでみます。
for (int i = 0; i < 5; i++) { // 50000行 10列 var table = Create(50000, 10); table.DefaultView.Sort = "COLUMN_0, COLUMN_1"; // インデックス作っておく Watch(() => { var rows = table.Select("COLUMN_0 = 'DATA_10' OR COLUMN_1 like 'DATA_1%'", "COLUMN_0"); Console.WriteLine("{0}行見つかりました", rows.Length); }); }
実行すると以下のような結果になります。
5857行見つかりました かかった時間: 64ms 5906行見つかりました かかった時間: 72ms 5982行見つかりました かかった時間: 62ms 5925行見つかりました かかった時間: 76ms 5969行見つかりました かかった時間: 64ms
凄く早くなっています!!さすがインデックスの力。ただ、ちょっとこのプログラムはズルしてて、インデックスを作る処理を計測時間に入れていません。ということで、以下のようにプログラムを変えてインデックスを作る処理も計測範囲に入れてしまいましょう。
for (int i = 0; i < 5; i++) { // 50000行 10列 var table = Create(50000, 10); Watch(() => { table.DefaultView.Sort = "COLUMN_0, COLUMN_1"; // インデックス作っておく var rows = table.Select("COLUMN_0 = 'DATA_10' OR COLUMN_1 like 'DATA_1%'", "COLUMN_0"); Console.WriteLine("{0}行見つかりました", rows.Length); }); }
実行すると…
5958行見つかりました かかった時間: 354ms 6037行見つかりました かかった時間: 386ms 6027行見つかりました かかった時間: 395ms 5890行見つかりました かかった時間: 347ms 5977行見つかりました かかった時間: 353ms
ちょっと残念な結果になってしまいました。インデックスを作るのに、結構な時間がかかってそうです。(まぁインデックス作るのにDataTableの中身最低でも1回は総なめしないといけないから当然っちゃ当然です)
なので、このDataViewで事前にインデックスを作っておく方法は、同一のDataTableに対して、複数回Selectを呼ぶときに効果を発揮します。今回みたいに一回きり検索しかしないときは、逆にインデックスの生成が負担になってしまいます。
DataViewを作ったときのちょっとした罠
さらにDataViewでインデックスを作るときに注意することがあります。インデックス作ってしまうと、データの書き込み時にインデックスの更新が入ってしまうためか、書き込みのパフォーマンスが悪化してしまいます。(データ量に依存するのかどうかまでは、調べてません)
ちょっと実際に見てみましょう。
for (int i = 0; i < 5; i++) { // 50000行 10列 var table = Create(50000, 10); Watch(() => { // 全データ書き換え foreach (var row in table.Rows.Cast<DataRow>()) { row.BeginEdit(); foreach (var col in table.Columns.Cast<DataColumn>()) { row[col] = "あばばばばばばば"; } row.EndEdit(); } }); }
DataViewを使わない状態で、全データを書き換えています。実行結果は以下のようになりました。
かかった時間: 101ms かかった時間: 107ms かかった時間: 99ms かかった時間: 106ms かかった時間: 103ms
大体100msかかっています。50万箇所書き換えてる割には優秀だと思われます。これを、データ書き換えの前にDataViewにインデックス作らせるように仕向けたプログラムになおして実行してみます。
for (int i = 0; i < 5; i++) { // 50000行 10列 var table = Create(50000, 10); table.DefaultView.Sort = "COLUMN_0, COLUMN_1"; // インデックス作っておく Watch(() => { // 全データ書き換え foreach (var row in table.Rows.Cast<DataRow>()) { row.BeginEdit(); foreach (var col in table.Columns.Cast<DataColumn>()) { row[col] = "あばばばばばばば"; } row.EndEdit(); } }); }
実行結果を見ると・・・
かかった時間: 817ms かかった時間: 839ms かかった時間: 822ms かかった時間: 826ms かかった時間: 813ms
8倍くらい時間がかかってます。ということで、データを書き換えることが多いときは、DefaultViewにインデックス作ってしまうと悲しいことになるかもしれません。
LINQ to DataSet
ついに本命LINQ to DataSetです。さくっとプログラムは作ってみました。上で書いてるSelectを使ったものと同じ条件になってるはずです。
for (int i = 0; i < 5; i++) { // 50000行 10列 var table = Create(50000, 10); Watch(() => { var rows = ( from row in table.AsEnumerable() let column0 = row.Field<string>("COLUMN_0") let column1 = row.Field<string>("COLUMN_1") where column0 == "DATA_10" || column1.StartsWith("DATA_1") orderby column0 select row ).ToArray(); Console.WriteLine("{0}行見つかりました", rows.Length); }); }
実行してみると・・・
5905行見つかりました かかった時間: 50ms 5860行見つかりました かかった時間: 46ms 5896行見つかりました かかった時間: 43ms 5793行見つかりました かかった時間: 58ms 5940行見つかりました かかった時間: 56ms
ちょっとびっくり。インデックスを使うようにしたSelectメソッドよりも早いです。これは、文字列で渡されたクエリをパースして検索条件として組み立ててる処理がSelect内部で行われてるせいなのだろうか??
まとめ
とりあえず、LINQ to DataSetを使えるときには使っておくとよさげ。