DOBON.NET DOBON.NETプログラミング掲示板過去ログ

DataGirdViewへのDataTable表示を早くしたい

環境/言語:[OS : Windows 7 / 言語 : C# / .NET Framework : 3.5]
分類:[.NET]

【解決したい問題】

DataTable型の変数 TableA を

dataGridView1.DataSource = TableA ;

という処理をしているのですが、TableA が大量のデータだと
上記の処理にとても時間がかかってしまいます。

これを早く行うには、どうしたらよいでしょうか?


もともと、DataGridViewでの行削除→削除行の復帰 ということをしたくて、
削除実行時に、dataGridView1.Rows.Remove();の前に
TableA に dataGridView1 のDataTableを保持しておき、
[戻す]ボタンクリック時に、dataGridView1.DataSource = dtTableA ;
を実行しようとしたら、とても時間がかかるという事態に至りました。

削除行の復帰には、もっと適した方法があるのでしょうか。
こちらも合わせてアドバイス願います!
.NET Framework のバージョンが違ったので修正させてください。
.NET Framework : 4.0
■No29448に返信(みなさんの記事)
> TableA が大量のデータだと
具体的には、どのぐらいの量なのでしょうか?

> 上記の処理にとても時間がかかってしまいます。
どのくらいかかっていて、それをどの程度まで減らしたいのでしょうか?


ちなみに、下記の50万件データで試してみたところ、
DataTable の構築が 6.2 秒、表示は 3.1 秒でした。


DataTable tbl = new DataTable();
private void Form1_Load(object sender, EventArgs e)
{
    tbl.Columns.Add("ID", typeof(int));
    tbl.Columns.Add("Text");
    tbl.Columns.Add("X", typeof(bool));
    tbl.PrimaryKey = new DataColumn[] { tbl.Columns["ID"] };

    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i <= 500000; i++)
    {
        tbl.Rows.Add(i, i.ToString("0000000"), (i % 2) == 0);
    }
    sw.Stop();
    label1.Text = sw.Elapsed.TotalSeconds.ToString();
}

private void button1_Click(object sender, EventArgs e)
{
    Stopwatch sw = Stopwatch.StartNew();
    dataGridView1.DataSource = tbl;
    sw.Stop();
    label2.Text = sw.Elapsed.TotalSeconds.ToString();
}



> これを早く行うには、どうしたらよいでしょうか?
そもそも、大量のデータを表示させない事です。
一度に表示させるデータ量を絞り込みましょう。


DataGridView の仮想モードを使うという選択肢があるかもしれませんが、
たとえ処理速度が高速になっても、あまりに膨大なデータが表示されていては、
使い勝手の良くないインターフェイスになってしまうでしょう。

たとえば 50 万件のデータが表示されていたとして、ユーザーは、
その中から、目的の行(たとえば ID=123456 な行)まで、スクロールバーで、
辿りつけるでしょうか。マウスやキーボードの操作では限界があるはずです。



> DataGridViewでの行削除→削除行の復帰 ということをしたくて、

RowFilter 等を使えないかな、と思いましたが、
50万件 → 25 万件のフィルターで 1.8 秒、
25万件 → 50 万件に復元するのに 2.6 秒でした。

  private void button2_Click(object sender, EventArgs e)
  {
      Stopwatch sw = Stopwatch.StartNew();
      tbl.DefaultView.RowFilter = "X=FALSE";
      sw.Stop();
      label3.Text = sw.Elapsed.TotalSeconds.ToString();
  }

  private void button3_Click(object sender, EventArgs e)
  {
      Stopwatch sw = Stopwatch.StartNew();
      tbl.DefaultView.RowFilter = "";
      sw.Stop();
      label3.Text = sw.Elapsed.TotalSeconds.ToString();
  }
> 具体的には、どのぐらいの量なのでしょうか?
25列、1500行のテーブルです。


> どのくらいかかっていて、それをどの程度まで減らしたいのでしょうか?
上記の25列、1500行のテーブルデータの場合で、現状63秒程かかっています。
出来るだけ、短くしたいのですが…かかっても2、3秒とか。
[戻す]ボタンから実際に表示が戻るまでのストレスをなくすことが目的です。

> ちなみに、下記の50万件データで試してみたところ、
> DataTable の構築が 6.2 秒、表示は 3.1 秒でした。
Columns数が違うことが、時間の差になっているのでしょうか?
50万件で6.2秒とは…
■No29454に返信(みなさんの記事)
> 50万件で6.2秒とは…

DataGirdView への表示が遅いことを問題にしているのではなく、
実は DataTable の構築が遅いことを問題にしていたのでしょうか?

私のコードは見て分かるように、その内容は検証用の固定的なデータです。

一方、そちらは何らかのデータベースから取得しているのでは無いでしょうか。
もしも DB からの取得なのだとしたら、
 ・データベースへの接続にかかる時間
 ・SQL を実行して、結果が返され始めるまでの時間
 ・データをすべて取得するまでの時間
 ・それを DataTable に格納するまでの時間
のうち、どこがボトルネックになっているのかを調べる必要があるでしょう。

ただしADO.NET では、(ADO とは異なり)上記の処理をまとめて行うことが
多いので、原因箇所を絞り込みにくいかと思います。その場合には、
SQL Server Management Studio や Microsoft Access 等のツールを使う事で、
「SQL を実行してから、結果が返却され始めるまでの時間」と
「返却結果の最後のレコードまで取得完了するまでの時間」を
ある程度計測できると思います。ODBC の場合はログ機能を使うのも手です。

SQL の実行そのものに時間がかかっているのであれば、実行計画を調査したり、
SQL 文にインデックスヒント文を埋め込んだり、あるいはインデックス最適化や
そもそものデータベース定義を見直すなど、何らかのチューニングが必要です。

SQL の実行開始は遅くないけれども、最後のレコードまで表示し終わるのに
時間がかかる場合は、ネットワーク負荷の見直しや、DBドライバの更新が
必要なこともあります。(ADO.NET ではなく ADO の場合ですが、過去に
ODBC ドライバや OLEDB プロバイダを、サードパーティー製の有償製品に
変更することで、パフォーマンスアップを果たしたことがあります)

そして、データの列挙自体は遅くないのに、DataTable への格納が遅い場合は
単純に転送すべきデータ量の問題が考えられます。Access 等の場合は、
現在画面に表示されている件数+α程度のデータさえキャッシュすれば
良いのに対し、DataTable の場合は全レコードのデータの静的コピーが
必要となります。そのため、大量のレコードを扱うことには向いていません。


> Columns数が違うことが、時間の差になっているのでしょうか?
差が生じるとすれば、列数というよりもデータ量の方が大きいと思います。
1 レコードあたりのデータ量は、平均で何バイトぐらいですか?
(データベース側のサイズでは無く、取得後の .NET 側のサイズとして)

decimal なら 16 バイトを必要としますし、
string なら、およそ文字数×2バイトでしょう。

実際にはこれに、DataTable 側の管理データ分が載るとは思いますが、
いずれにせよデータ量が多くなれば、その分、メモリ確保にも
より多くの時間がかかる道理です。


なお、DataTable 側ではなく DataGridView 側としてみた場合は、
行数の増加よりも列数の増加の方が負荷は高いと思います(未計測)。
とはいえ今回の場合で言えば、3列→25列に増加した分の負荷よりも、
1500行→50万行の負荷の方が高いでしょうけれども。
> DataGirdView への表示が遅いことを問題にしているのではなく、
> 実は DataTable の構築が遅いことを問題にしていたのでしょうか?

理解できておらず恐縮です。
ご指摘と説明を拝見して、DataTable の構築が遅いことを問題にしていた
ことを理解しました。

行の削除と復旧について、改めます。
2011/12/09(Fri) 13:02:58 編集(投稿者)

おそらく列数がこれだけ多いのであれば、DataTableの取得より、DataGirdViewの表示の方がはるかに時間がかかっているのではないでしょうか?
一度、DataTable取得までの時間とDataGirdViewの表示までの時間を計ることをお勧めします。

ところで、DataGirdViewにデータを表示するとき、AutoSizeColumnsModeやAutoSizeRowsModeのプロパティを自動調整にしていませんか?
これを使うと、DataGirdViewの表示に非常に時間がかかります。

http://dobon.net/vb/dotnet/datagridview/autosizecolumn.html

> 一度、DataTable取得までの時間とDataGirdViewの表示までの時間を計ることをお勧めします。

dataGridView1.DataSource =table ;
にブレイクをはり、次のステップへ移るまでに60秒以上かかっています。
その他に長時間かかるところはありませんでした。


> ところで、DataGirdViewにデータを表示するとき、AutoSizeColumnsModeやAutoSizeRowsModeのプロパティを自動調整にしていませんか?
AutoSizeColumnsModeをFillにしていましたが、Noneにしてもかかる時間に
変化はありませんでした。
2011/12/09(Fri) 14:10:42 編集(投稿者)

■No29457に返信(みなさんの記事)
> ご指摘と説明を拝見して、DataTable の構築が遅いことを問題にしていた
> ことを理解しました。
実際のデータ内容が分からないので、比較にはならないと思いますが、
| 
| "1-1", "1-2", …, "1-25"
|     :
| "1500-1", "1500-2", …, "1500-25"
|
のような 25列 1500行 のデータを表示させる限りにおいては、
 dataGridView1.DataSource = dataTable1;
の操作で、 0.1 秒以内に表示できました。

dataGridView 側で追加の処理やスタイル設定がある場合や、
各列のデータ量が多い場合には、もう少し時間が長くなるかも知れませんが、
DataSource の割り当てだけで 1 分以上かかることは、そうそう無いように思います。


> 行の削除と復旧について、改めます。
データベース側でのデータ削除では無く、DataTable 側の削除のことなのですよね。
RejectChanges メソッドを呼び出すことで復元できないでしょうか。

DataGridView に対してキーボードの [Delete] キーで削除した場合、
DataTable 上のそれぞれの行(DataRow)の RowState プロパティは、

・新規追加された行( DataRowState.Added )だった場合
 → [Delete]すると、DataTable から物理削除 ( DataRowState.Detached )
  → さらに RejectChanges すると、未登録状態 ( DataRowState.Detached ) に。

・データ取得直後で未編集の行( DataRowState.Unchanged )だった場合
 → [Delete]しても、削除マークがつくだけ ( DataRowState.Deleted )
  → さらに RejectChanges すると、未編集 ( DataRowState.Unchanged ) に戻る。

・内容が編集された状態の行( DataRowState.Modified )だった場合
 → [Delete]しても、削除マークがつくだけ ( DataRowState.Deleted )
  → さらに RejectChanges すると、未編集 ( DataRowState.Unchanged ) に戻る。

という状態に変化します。

また、削除マークがついただけの行の内容を表示するために、
 DataView dv = new DataView( dataTable1 );
 dv.RowStateFilter = DataViewRowState.Deleted;
 dataGridView1.DataSource = dv;
 dataGridView1.ReadOnly = true;
のようにすることも可能です。
> 後者だとしたら、DataTable に対して RejectChanges を呼び出せば復元されます。
> また、削除マークがついただけの行の内容を表示するために、
>  DataView dv = new DataView( dataTable1 );
>  dv.RowStateFilter = DataViewRowState.Deleted;
>  dataGridView1.DataSource = dv;
>  dataGridView1.ReadOnly = true;
> のようにすることも可能です。

上記実行しても何も表示されないので、Detached のようです。

削除はdataGridView1.Rows.Remove()で行っているので物理削除
なのだと。

/////////////////////////////////////////////////////
// DataGridView1で選択されているすべての行を削除.
/////////////////////////////////////////////////////
foreach (DataGridViewRow r in dataGridView1.SelectedRows)
{
if (!r.IsNewRow)
{
dataGridView1.Rows.Remove(r);

}
}

dataGridView1.Rows.Remove()ではなく、DataViewRowState.Deleted
にする方法があるでしょうか。
■No29464に返信(みなさんの記事)
> 削除はdataGridView1.Rows.Remove()で行っているので物理削除
> なのだと。

削除方法は関係ありません。
 ・dataGridView1.Rows.RemoveAt(n);
 ・dataGridView1.Rows.Remove(row);
 ・ユーザーによる [Delete] キーでの削除
これらはすべて、同じ結果となります。

No29462 に書いたように、物理削除になるかどうかは、
削除前の DataRow の元の RowState に依存しています。


No29452 のコードでは、そのまま削除すると物理削除されますが、
これはサンプルデータの各行が、追加行を表す Added になっているからです。

あらかじめ、元の DataTable に対して AcceptChanges メソッドを
呼んでおいてください。これにより、Added な行が 未編集を表す
Unchanged になります。

一度 Unchanged にしておけば、そのまま削除を実行しても、
物理削除にはなりません。Deleted な行になるだけです。

DOBON.NET | プログラミング道 | プログラミング掲示板