かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

DataTableからのデータ抽出方法の性能比較

## 追記

.NET 6 版を書きました

zenn.dev


## 本文

注意)あまり真面目に測ってません

今日も色々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を使えるときには使っておくとよさげ。