読者です 読者をやめる 読者になる 読者になる

ぴよぴよエンジニアの日記

クラウドベンダーに勤める見習いSEの日記です。発言は私自身の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

DataTable.AsEnumerable().Where() のパフォーマンス改善

DataTable.AsEnumerable().Where() のパフォーマンスが激しく悪かったので改善を試みました.

以下の記事を参考にさせて頂きました.

[.NET][C#]当然っちゃ当然だけどDataTableとか使いようによっては遅い
DataTableからのデータ抽出方法の性能比較 - かずきのBlog@hatena



データ構造

[double column0, double column1, ..., double column 9]

上記のようなカラムを持つデータセット.カラム数は全部で10個です.



データセット作成メソッド
// ダミーデータの入った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, typeof(double));
    }

    // ダミーデータの作成 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] = random.NextDouble();
        }
        result.Rows.Add(addRow);
    }
    return result;
}

リンクの記事をほぼそのまま使わせて頂きました.

1点変更を加えたのは,それぞれのカラムには全て double 型を指定しています.



列名でアクセス
double value = 0.1;

for (int i = 0; i < 5; i++)
{
    // 2500000行 10列
    var table = Create(2500000, 10);
    Watch(() =>
    {
        var rows = table.AsEnumerable().Where(v => v.Field<double>("COLUMN_0") >= (value - 0.0001) 
            && v.Field<double>("COLUMN_0") <= (value + 0.0001)
            && v.Field<double>("COLUMN_1") >= (value - 0.0001)
            && v.Field<double>("COLUMN_1") <= (value + 0.0001));
        Console.WriteLine("{0}行見つかりました", rows.ToArray().Length);
    });
}

DataRow に列名でアクセスします.

0行見つかりました
かかった時間: 535ms
0行見つかりました
かかった時間: 525ms
0行見つかりました
かかった時間: 519ms
0行見つかりました
かかった時間: 519ms
0行見つかりました
かかった時間: 532ms

520~530sといったところでしょうか.

1回だけの検索ならば気にするほどの遅さではないかもしれませんが,
今回は1万回以上検索させたいのでもっとパフォーマンスを上げたいところです.



インデックスでアクセス
double value = 0.1;

for (int i = 0; i < 5; i++)
{
    // 2500000行 10列
    var table = Create(2500000, 10);
    Watch(() =>
    {
        var rows = table.AsEnumerable().Where(v => v.Field<double>(0) >= (value - 0.0001) 
            && v.Field<double>(0) <= (value + 0.0001)
            && v.Field<double>(1) >= (value - 0.0001)
            && v.Field<double>(1) <= (value + 0.0001));
        Console.WriteLine("{0}行見つかりました", rows.ToArray().Length);
    });
}

参考ページにあったように列名でなく,インデックスで DataRow にアクセスしてみます.

ループ内の処理ではありませんが,Where メソッド内でも効果があるのか...

0行見つかりました
かかった時間: 351ms
2行見つかりました
かかった時間: 356ms
0行見つかりました
かかった時間: 359ms
0行見つかりました
かかった時間: 341ms
0行見つかりました
かかった時間: 329ms

350msといったところでしょうか,明らかに改善しています!

Where メソッドでも効果は健在のようです.



インデックスでアクセス かつ 条件式を統合
double value = 0.1;

for (int i = 0; i < 5; i++)
{
    // 2500000行 10列
    var table = Create(2500000, 10);
    Watch(() =>
    {
        var rows = table.AsEnumerable().Where(v => Math.Abs(v.Field<double>(0) - value) <= 0.0001 
            && Math.Abs(v.Field<double>(1) - value) <= 0.0001);
        Console.WriteLine("{0}行見つかりました", rows.ToArray().Length);
    });
}

列名でアクセスし,かつ条件式をMath.Abs()を使い統合しています.

この条件式だとインデックスが使えないので遅くなると思いきや...

1行見つかりました
かかった時間: 302ms
0行見つかりました
かかった時間: 259ms
0行見つかりました
かかった時間: 246ms
0行見つかりました
かかった時間: 253ms
0行見つかりました
かかった時間: 248ms

さらに改善している!Σ

最初の列名アクセスに比べるとほぼ半分の時間です.
これは驚異的です.

SQL で条件式の左辺を計算してしまうとインデックスが使えなくなってしまいますが,LINQはそうではない...?

というかそもそもインデックス使ってるのか?フルスキャンか?
あと,AND の処理は内部的にはどう結合しているんだ?

LINQ の内部処理が気になります...