スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
拍手コメントを見る

.NET DataTable の集計速度を計測してみた

.NET の DataTable では Group By 的な事をやろうとしても、そういったメソッドは用意されていません。
実現方法としていくつか思いついたのですが、本格実装に入る前に、どの方法が一番パフォーマンスに優れているのか計測してみました。
※ちなみに汎用的な方法ではないので期待せぬよう。

例として次のようなテーブルを集計対象とします。
+--------+--------+-------+--------+
| Shop | Item | Price | Number |
+--------+--------+-------+--------+
| Odakyu | Apple | 200 | 500 |
| Odakyu | Melon | 600 | 100 |
| Odakyu | Orange | 150 | 300 |
| Seibu | Apple | 180 | 400 |
| Seibu | Orange | 160 | 600 |
| Tokyu | Melon | 700 | 50 |
| Tokyu | Orange | 170 | 200 |
+--------+--------+-------+--------+

Shop には仕入先の名称が入り、Itemには品物、Price には品物の単価、Number には仕入数。
というテーブル構造です。

これらの商品を仕入れた店では Apple の仕入件数は何件?などを「集計」したいとします。
上記の表から Apple が入っている行(レコード)を対象として、その行数(レコード数)が分かれば、Apple の仕入件数が何件あるのかが導き出せます。
+--------+--------+-------+--------+
| Shop | Item | Price | Number |
+--------+--------+-------+--------+
| Odakyu | Apple | 200 | 500 |
| Seibu | Apple | 180 | 400 |
+--------+--------+-------+--------+

結果としてはこの様に2行あるので、仕入件数は2件ということになります。

SQL の場合は、これらの集計は簡単で、次のように記述することで結果を得る事ができます。
SELECT COUNT(*) FROM MyTable WHERE Item='Apple'

各Itemで集計したい場合は、次のように記述します。
SELECT Item, COUNT(Item) FROM MyTable GROUP BY Item

結果は次のようになります。
+--------+-------+
| Item | Count |
+--------+-------+
| Apple | 2 |
| Melon | 2 |
| Orange | 3 |
+--------+-------+

この様な結果を得られるよう、DataTable 上のレコードに対して処理を行う場合はそれなりの手順を踏む必要があります。

以下に、思いついた3つの例をあげますが、得られる結果は全て同じです。
コーディング上、一番スッキリするのは次の書き方だと思います。

■ DefaultView.ToTable() を利用する方法
001://
002:// Example1
003:// DefaultView.ToTable() を利用する方法
004://
005:private DataTable example1()
006:{
007: DataTable dtChild = dtSrc.DefaultView.ToTable("dtChild", false, "Item");
008: DataTable dtParent = dtSrc.DefaultView.ToTable("dtParent", true, "Item");
009: dtParent.Columns.Add(new DataColumn("Count", typeof(int)));
010:
011: DataSet ds = new DataSet();
012: ds.Tables.Add(dtParent);
013: ds.Tables.Add(dtChild);
014:
015: DataRelation rel = new DataRelation("rel",
016: dtParent.Columns["Item"],
017: dtChild.Columns["Item"],
018: false);
019:
020: ds.Relations.Add(rel);
021:
022: dtParent.Columns["Count"].Expression = "Count(Child.Item)";
023:
024: return dtParent.DefaultView.ToTable();
025:}

これは、DeafultView.ToTable() の引数に distinct が指定できる事を利用しています。
順不同ですが、やっていることは大体次のようなことです。
007行では、dtSrc の "Item" 列だけを採用し、全レコードを dtChild に代入しています。
008行では、dtSrc の "Item" 列だけを採用し、かつ重複しないレコードが dtParent に代入されます。
dtParent と dtChild を DataSet に加え、親子関係を設定します。
dtParent に集計用の列"Count"を追加し、そこに dtChild の集計値を代入するように計算式を指定します。

以上の処理手順により dtParent には各項目とその集計数が入ります。

------

次の方法は、元テーブルのレコードを逐一参照して、集計結果を別テーブルに代入するという方法です。

■ DataTable.Select() を利用する方法
001://
002:// Example2
003:// DataTable.Select() を利用する方法
004://
005:private DataTable example2()
006:{
007: DataTable dtRet = new DataTable();
008: dtRet.Columns.Add(new DataColumn("Item", typeof(string)));
009: dtRet.Columns.Add(new DataColumn("Count", typeof(int)));
010:
011: dtRet.PrimaryKey = new DataColumn [] { dtRet.Columns["Item"] };
012:
013: foreach(DataRow drSrc in dtSrc.Rows)
014: {
015: DataRow drRet;
016: string exp = string.Format("Item='{0}'", drSrc["Item"]);
017: DataRow[] drs = dtRet.Select(exp);
018:
019: if (0 == drs.Length)
020: {
021: drRet = dtRet.NewRow();
022: drRet["Item"] = drSrc["Item"];
023: drRet["Count"] = 1;
024:
025: dtRet.Rows.Add(drRet);
026: }
027: else
028: {
029: drRet = drs[0];
030: drRet["Count"] = (int)drRet["Count"] + 1;
031: }
032: }
033:
034: return dtRet;
035:}

集計結果は、dtRet テーブルに入ります。dtRet 上の Item 列の値は重複を許可していません。そのため、019行の Select() メソッドの呼び出しでは、結果が0行か1行しか返ってきません。
dtSrc テーブルの全ての行を対象として、指定列の値に一致するものがなければ新しい行を dtRet に追加(023行~)し、一致するものあれば、既存の"Count" 値を増加させています(029行~)。

------

残る最後の方法は、Example2 とほぼ同じですが、既存レコードの検索に DataTable.Rows.Find() を利用した方法です。
集計結果のテーブル dtRet には、重複するItemが許可されていないという事ならば、Select() メソッドで複数行返ってくる可能性を考慮せずに、単一行を処理した方がスマートなのでは?と考えたのと、Select() と Rows.Find() にどれだけ差がでるのか興味がわいたので、実装してみました。

■ DataTable.Rows.Find() を利用する方法
001://
002:// Example3
003:// DataTable.Rows.Find() を利用する方法
004://
005:private DataTable example3()
006:{
007: DataTable dtRet = new DataTable();
008: dtRet.Columns.Add(new DataColumn("Item", typeof(string)));
009: dtRet.Columns.Add(new DataColumn("Count", typeof(int)));
010:
011: dtRet.PrimaryKey = new DataColumn [] { dtRet.Columns["Item"] };
012:
013: foreach(DataRow drSrc in dtSrc.Rows)
014: {
015: object [] objs = new object[] { drSrc["Item"] };
016: DataRow drRet = dtRet.Rows.Find(objs);
017:
018: if (null == drRet)
019: {
020: drRet = dtRet.NewRow();
021: drRet["Item"] = drSrc["Item"];
022: drRet["Count"] = 1;
023:
024: dtRet.Rows.Add(drRet);
025: }
026: else
027: {
028: drRet["Count"] = (int)drRet["Count"] + 1;
029: }
030: }
031:
032: return dtRet;
033:}


以上の3例を使ってのパフォーマンスの計測結果です。
           +--------------------------------+
| 対象件数 |
+-------+-------+-------+--------+
| 100 | 1000 | 10000 | 100000 |
+----------+-------+-------+-------+--------+
| Example1 | 0.062 | 0.109 | 1.546 | 35.907 |
| Example2 | 0.015 | 0.031 | 0.406 | 5.203 |
| Example3 | 0.000 | 0.015 | 0.140 | 1.703 |
+----------+-------+-------+-------+--------+
※処理に掛った時間(秒)です。
※参考:Windows XP, Intel Core2 2GHz


Example1 が極端に遅い事がわかります。10万件のレコードに対しての集計結果におよそ36秒掛っています。
データ量が多くなってくると顕著ですが、この3例では、Example3 が最も優れている結果となりました。
ちなみに、100万行ほど処理させた結果は次の通りです。
Example1 : 421.258 (7m01s)
Example2 : 52.001 (0m52s)
Example3 : 16.766 (0m17s)


Example1 コードのどこに問題があるのか細部の計測を行ってみたところ、ボトルネックとなっていたのは、008行の DefaultView.ToTable(, true, ) の重複しないレコードを抽出する部分でした。
Example1 ではこの部分がキモであるにも関わらず、上記のようなパフォーマンスの悪さからすると、残念ながら使う場所を限定せざるを得ないレベルのようです。

また、Example2 と比較すると2~3倍程度高速なのが Example3 です。
単一行しかないと分かっているならば、Select を使うよりも Rows.Find() を利用するのが良い様です。

なお、これは、VisualStudio2005 で実験しています。
拍手コメントを見る

テーマ : プログラミング
ジャンル : コンピュータ

trackback


この記事にトラックバックする(FC2ブログユーザー)

DataView.ToTable() の distinct 指定が遅い

以前のエントリ「.NET DataTable の集計速度を計測してみた」でも書きましたが、DataView.ToTable() メソッドにおける、distict 指定がパフォーマンス上、...

コメント

非公開コメント
ブログ内検索
プロフィール

雷ぶ

Author:雷ぶ

最近の記事
最近のコメント
最近のトラックバック
カテゴリー
月間アーカイブ
ブロとも申請フォーム

この人とブロともになる

RSSフィード
リンク

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。