QA@IT

C#で特定の列でDataTableの比較

19205 PV

C#で下記のような2つのDataTableの比較をし、column2に対してTable1のみ存在するデータの取得処理を作成しています。


Table1:column1,column2,column3,column4
Table2:column1,column2,column3,column4
List<DataTable> list = new List<DataTable>();
for(int i =0;i<Tabe1.Row.Count;i++)
{
    for(int j =0;j<Tabe2.Row.Count;j++)
    {
       if(Table1.Row[i][colmun2] != Table2.Row[j][colmun2])
       {
            list.Add(Table1);
            break;
       }
    }
}

一応上記のやり方でcolumn2に対して、Table1のみ存在するデータが取得できますが、
テーブルのデータが大量だと処理時間がかかってしまいます。
処理時間を早める方法はありますでしょうか?
追記:当初のコードが同じデータが存在する場合となっていたので、修正しました。

  • 直してくれたのに水を差すようですが、修正されたコードでも意図したデータは取れないと思います。 -

回答

コードの内容はTable1に存在するcolumn2が、Table2のcolumn2と一致する数だけTable1をAddしていますね。

var list = (from p in Table1.AsEnumerable()
            where !((from q in Table2.AsEnumerable()
                select q.column2).Contains(p.column2))
            select p).ToList();

LINQを用います。SQL文のNOT EXISTS句を再現することで
「Table1とTable2のうちTable1にのみ存在するcolumn2を持つ行」
を返すことができると思います。多分これが一番早いと思います。

編集 履歴 (3)
  • 返答が遅くなってすみません。
    コードの内容が間違っているのにも関わらず回答ありがとうございます。
    上記方法で比較することが出来ました。ありがとうございました。
    -
  • いえいえ。よくよく考えるとこの方法は多分一番遅いです。
    flied_onionさんのHashSet<T>のほうが2000倍ぐらい早いです。
    -

他の方も指摘されていますがコードが元々の話と異なっているので、
とりあえず提示された条件に加えて以下のような条件だとして考えます。

  • データ格納済みの素のDataTableである。
  • 列はとりあえずすべて文字列とする(決まってないとやりにくいので)。
  • Table1側のDataRowのListを抽出する。
  • .NET 3.5以降

で、テスト用のデータテーブルは以下のメソッドで生成でされるようなものとします。
ループでDataTableSize件分のDataTableを作成しますが、引数にtrueを与えるとループカウンタの奇数をスキップして、レコードが半分になります。

const int DataTableSize = 7000;

DataTable GenerateTable(bool oddSkip) {

    var dt = new DataTable();
    dt.Columns.Add("column1", typeof(String));
    dt.Columns.Add("column2", typeof(String));
    dt.Columns.Add("column3", typeof(String));
    dt.Columns.Add("column4", typeof(String));

    var step = (oddSkip ? 2 : 1);
    for (var i = 0; i < DataTableSize; i += step) {
        var r = dt.NewRow();
        r["column1"] = i.ToString();
        r["column2"] = i.ToString();
        r["column3"] = i.ToString();
        r["column4"] = i.ToString();
        dt.Rows.Add(r);
    }

    dt.AcceptChanges();
    return dt;
}

これを用いてTable1とTable2は以下のように作成します。

var Table1 = GenerateTable(false);
var Table2 = GenerateTable(true);

以降ここの説明では Table1, Table2 を使用することとします。


このTable1, Table2についてfor文やforeachを用いた場合は以下のようになります。
そしてこれが現在の状態だと思います。
そしてこれは最悪の場合 7000 × 3500のループとなります。

var list = new List<DataRow>();

foreach (DataRow t1 in Table1.Rows) {
    var exists = false;
    foreach (DataRow t2 in Table2.Rows) {
        if ((string)t1["column2"] == (string)t2["column2"]) {
            exists = true;
            break;
        }
    }
    if (!exists) {
        list.Add(t1);
    }
}

return list;

これを高速化するにはどうするかですが、HashSetを使うか、Except LINQ拡張メソッドを使うかがいいのではないかと思います。
HashSetの方が簡単に書けるのでまずはHashSetで示します。

HashSetを使う

HashSet<T>はキーだけを格納するDictionary<T>のイメージです。
.NET 3.5未満の場合はHashSetがないのでその場合はDictionaryで代用できますが、Dictionaryは重複したキーが登録されると例外が発生するのでちょっと注意が必要です(HashSetは例外は起きません)。

var hs = new HashSet<string>();

foreach (DataRow dr in Table2.Rows) {
    hs.Add((string)dr["column2"]);
}

var list = Table1.AsEnumerable()
             .Where(r => !hs.Contains((string)r["column2"]))
             .ToList();

// // LINQ を使いたくない場合は以下
// var list = new List<DataRow>();
// foreach (DataRow dr in Table1.Rows) {
//     if (!hs.Contains((string)dr["column2"])) {
//         list.Add(dr);
//     }
// }

return list;

これだとループは 3500 + 7000 になり、Containsもハッシュ値で行われるので高速です。
(私の環境では、元の総当たり検査から100分の1 ぐらいになりました)


Exceptを使う

LINQ拡張メソッドにExceptという、SQLでいうMINUS演算にあたるまさにそのものなメソッドがありますが、
単純でないオブジェクトの比較、今回でいえば DataRowの中の特定の列を比べたいというような場合、
比較用のクラスを作成する必要があるので手軽とは言えません。(ただのList<String>などの場合はもっと手軽に利用できます。)

具体的には以下のような EqualityComparerを継承したクラスを用意しておいて

class Column2Comparer : EqualityComparer<DataRow> {
    public override bool Equals(DataRow r1, DataRow r2) {
        // 比較方法はcolumn2の内容に応じて検討しなければならない。
        if (Object.Equals(r1["column2"], r2["column2"])) {
            return true;
        }
        return (r1["column2"] == r2["column2"]);
    }

    public override int GetHashCode(DataRow r) {
        return r["column2"].GetHashCode();
    }
}

以下のように使います。

var list = Table1.AsEnumerable()
          .Except(Table2.AsEnumerable(), new Column2Comparer())
          .ToList();
return list;

こちらも高速です。

なお、余談ですがただのListなどの場合は比較用クラスは不要で以下のようになります。

var l1 = new List<String>() { "a", "b", "c" };
var l2 = new List<String>() { "c", "d", "f" };

var minusList = l1.Except(l2);

Takagi_Hinko さんが示されているLINQ クエリ式(私の条件に合うように列へのアクセスを書き直しました)

var list = (from p in Table1.AsEnumerable()
            where !((from q in Table2.AsEnumerable()
                        select (string)q["column2"]).Contains((string)p["column2"]))
            select p).ToList();

return list;

ですとか、メソッド式を使って

return Table1.AsEnumerable()
    .Where(r1 => !Table2.AsEnumerable()
                        .Any(r2 => r1["column2"] == r2["column2"])
    ).ToList();

といった書き方は、結局双方を列挙して比較してますので元々のfor文とあまり変わらないと思います。
(Any使った方はfor文より遅くなりました。)

HashSetを使った場合リストが一つ増えますのでその点(メモリ消費)は注意が必要ですが速度の向上にはつながると思います。

編集 履歴 (6)
  • たくさんの回答ありがとうございます。
    Takagi_Hinkoさんの方法で比較することができましたが、今後同じような比較がありましたら参考にさせていただきます。
    -
ウォッチ

この質問への回答やコメントをメールでお知らせします。