web-dev-qa-db-ja.com

2つのDataTableを比較して、一方の行を決定し、もう一方の行を決定しない

CSVファイルから生成されたABの2つのDataTableがあります。 Bに存在しないAに存在する行を確認できるようにする必要があります。

さまざまな行を表示するために何らかのクエリを実行する方法はありますか、または各DataTableの各行を反復処理してそれらが同じかどうかを確認する必要がありますか?後者のオプションは、テーブルが大きくなると非常に集中的になるようです。

17
Jon

各DataTableの各行を反復処理して、それらが同じかどうかを確認する必要がありますか。

CSVファイルからデータを読み込んだので、インデックスなどは何もないので、ある時点で、コードであれ、ライブラリであれ、何かがすべての行を反復処理する必要があります。 、または何でも。

とにかく、これはアルゴリズムの質問です。これは私の専門ではありませんが、私の素朴なアプローチは次のようになります。

1:データのプロパティを活用できますか?各テーブルのすべての行は一意であり、両方を同じ基準でソートできますか?もしそうなら、これを行うことができます:

  • 両方のテーブルをIDでソートします(クイックソートなどの便利なものを使用します)。それらがすでにソートされている場合、あなたは大きな勝利を収めます。
  • 両方のテーブルを一度にステップ実行し、どちらかのテーブルのIDのギャップをスキップします。一致したIDは、重複したレコードを意味します。

これにより、(ソート時間* 2)+ 1パスでそれを行うことができるため、私のbig-O表記が正しければ、(何でもソート時間)+ O(m + n)になるでしょう。 。
(改訂:これは ΤΖΩΤΖΙΟΥが説明するアプローチです

2:代替アプローチ。データの大きさによっては、効率が上がる場合と下がる場合があります。

  • 表1を実行し、行ごとに、そのID(または計算されたハッシュコード、またはその行の他の一意のID)を辞書(または、ハッシュテーブルと呼びたい場合はハッシュテーブル)に貼り付けます。
  • 表2を実行し、各行について、ID(またはハッシュコードなど)が辞書に存在するかどうかを確認します。辞書が非常に高速であるという事実を利用しています-O(1)だと思いますか?この検索は非常に高速ですが、これらのすべての辞書挿入を実行する代償を払っています。 。

私は自分よりもアルゴリズムの知識が豊富な人がこれについて考えているものに興味があります:-)

9
Orion Edwards

適切なタイプのID列があると仮定します(つまり、ハッシュコードを与え、同等を実装します)-この例の文字列は、DataTablesに慣れていないため、すべてを見る時間がないため、少し疑似コードです。今すぐ:)

IEnumerable<string> idsInA = tableA.AsEnumerable().Select(row => (string)row["ID"]);
IEnumerable<string> idsInB = tableB.AsEnumerable().Select(row => (string)row["ID"]);
IEnumerable<string> bNotA = idsInB.Except(idsInA);
20
Jon Skeet

これを行うには、DataTableのMergeメソッドとGetChangesメソッドを使用できます。

A.Merge(B); // this will add to A any records that are in B but not A
return A.GetChanges(); // returns records originally only in B
7
MusiGenesis

これまでの回答では、重複する主キーを単に探していると想定しています。これは非常に簡単な問題です。たとえば、Merge()メソッドを使用できます。

しかし、私はあなたがあなたが重複したDataRowsを探していることを意味するというあなたの質問を理解します。 (問題の説明から、両方のテーブルがCSVファイルからインポートされているため、元の行には主キーの値がなく、インポート中に主キーがオートナンバーを介して割り当てられていると想定しています。)

単純な実装(Aの各行について、そのItemArrayをBの各行のそれと比較する)は、実際には計算コストがかかります。

これを行うはるかに安価な方法は、ハッシュアルゴリズムを使用することです。各DataRowについて、その列の文字列値を単一の文字列に連結し、その文字列に対してGetHashCode()を呼び出してint値を取得します。 Dictionary<int, DataRow>には、DataTable Bの各DataRowのハッシュコードをキーとするエントリが含まれています。次に、DataTable Aの各DataRowについて、ハッシュコードを計算し、それがディクショナリに含まれているかどうかを確認します。存在しない場合は、DataRowがDataTable Bに存在しないことがわかります。

このアプローチには2つの弱点があり、どちらも2つの文字列は等しくなくても同じハッシュコードを生成する可能性があるという事実から生じます。ハッシュがディクショナリにあるAの行を見つけた場合、ディクショナリのDataRowをチェックして、2つの行が本当に等しいことを確認する必要があります。

2番目の弱点はより深刻です。B内の2つの異なるDataRowが同じキー値にハッシュされる可能性は低いですが、可能です。このため、辞書は実際にはDictionary<int, List<DataRow>>、そして前の段落で説明したチェックをリスト内の各DataRowに対して実行する必要があります。

これを機能させるにはかなりの量の作業が必要ですが、これはO(m + n)アルゴリズムです。

4
Robert Rossney

CSVファイルを単に比較することはできませんbeforeそれらをDataTablesにロードしますか?

string[] a = System.IO.File.ReadAllLines(@"cvs_a.txt");
string[] b = System.IO.File.ReadAllLines(@"csv_b.txt");

// get the lines from b that are not in a
IEnumerable<string> diff = b.Except(a);

//... parse b into DataTable ...
1
withakay

これを解決する簡単な方法を見つけました。以前の「exceptメソッド」の回答とは異なり、exceptメソッドを2回使用しています。これにより、削除された行だけでなく、追加された行もわかります。メソッドを1つだけ使用する場合-違いは1つだけで、両方はわかりません。このコードはテストされ、機能します。下記参照

//Pass in your two datatables into your method

        //build the queries based on id.
        var qry1 = datatable1.AsEnumerable().Select(a => new { ID = a["ID"].ToString() });
        var qry2 = datatable2.AsEnumerable().Select(b => new { ID = b["ID"].ToString() });


        //detect row deletes - a row is in datatable1 except missing from datatable2
        var exceptAB = qry1.Except(qry2);

        //detect row inserts - a row is in datatable2 except missing from datatable1
        var exceptAB2 = qry2.Except(qry1);

次に、結果に対してコードを実行します

        if (exceptAB.Any())
        {
            foreach (var id in exceptAB)
            {
   //execute code here
            }


        }
        if (exceptAB2.Any())
        {
            foreach (var id in exceptAB2)
            {
//execute code here
            }



        }
1
NewCsharper

参考までに:

一般にアルゴリズムについて言えば、2つのセットの並べ替え(IDは通常)の比較はO(M * N/2)演算ではなく、2つのセットが順序付けされている場合はO(M + N)です。したがって、一方のテーブルを、もう一方の開始点へのポインタでスキャンし、次のようにします。

other_item= A.first()
only_in_B= empty_list()
for item in B:
    while other_item > item:
        other_item= A.next()
        if A.eof():
             only_in_B.add( all the remaining B items)
             return only_in_B
    if item < other_item:
         empty_list.append(item)
return only_in_B

上記のコードは明らかに疑似コードですが、自分でコーディングする場合は、一般的な要点がわかるはずです。

1
tzot

すべてのフィードバックをありがとう。

残念ながらインデックスはありません。私の状況についてもう少し詳しく説明します。

EU全体の7台のサーバーにインストールされるレポートプログラム(Crystalレポートに置き換わる)があります。これらのサーバーには、多くのレポートがあります(国によって同じではありません)。これらは、構成にXMLファイルを使用するコマンドラインアプリケーションによって呼び出されます。したがって、1つのXMLファイルで複数のレポートを呼び出すことができます。

コマンドラインアプリケーションは、夜間のプロセスによってスケジュールおよび制御されます。したがって、XMLファイルは複数の場所から呼び出すことができます。

CSVの目的は、使用されているすべてのレポートとそれらの呼び出し元のリストのリストを作成することです。

すべての参照のXMLファイルを調べ、スケジューリングプログラムにクエリを実行し、すべてのレポートのリストを作成しています。 (これは悪くないです)。

私が抱えている問題は、運用環境から削除された可能性のあるすべてのレポートのリストを保持する必要があることです。古いCSVと新しいデータを比較する必要があります。このため、私はそれをDataTablesに入れて情報を比較するのが最善であると考えました(これは間違ったアプローチである可能性があります。それを保持するオブジェクトを作成し、違いを比較してから、それらを反復処理することができると思います)。

各レポートに関するデータは次のとおりです。

文字列-タスク名文字列-アクション名Int-ActionID(単一のアクションが多数のレポート、つまりXMLファイルを呼び出すことができるため、アクションIDは複数のレコードに含めることができます)。文字列-文字列と呼ばれるXMLファイル-レポート名

MusiGenesisからのMergeのアイデアを試してみます(ありがとう)。 (Mergeが機能するかどうかわからない一部の投稿を再読しますが、以前に聞いたことがないので、何か新しいことを学ぶ必要があるため、試してみる価値があります)。

HashCodeのアイデアも興味深いですね。

すべてのアドバイスをありがとう。

1
Jon
public DataTable compareDataTables(DataTable First, DataTable Second)
{
        First.TableName = "FirstTable";
        Second.TableName = "SecondTable";

        //Create Empty Table
        DataTable table = new DataTable("Difference");
        DataTable table1 = new DataTable();
        try
        {
            //Must use a Dataset to make use of a DataRelation object
            using (DataSet ds4 = new DataSet())
            {
                //Add tables
                ds4.Tables.AddRange(new DataTable[] { First.Copy(), Second.Copy() });

                //Get Columns for DataRelation
                DataColumn[] firstcolumns = new DataColumn[ds4.Tables[0].Columns.Count];
                for (int i = 0; i < firstcolumns.Length; i++)
                {
                    firstcolumns[i] = ds4.Tables[0].Columns[i];
                }
                DataColumn[] secondcolumns = new DataColumn[ds4.Tables[1].Columns.Count];
                for (int i = 0; i < secondcolumns.Length; i++)
                {
                    secondcolumns[i] = ds4.Tables[1].Columns[i];
                }
                //Create DataRelation
                DataRelation r = new DataRelation(string.Empty, firstcolumns, secondcolumns, false);
                ds4.Relations.Add(r);
                //Create columns for return table
                for (int i = 0; i < First.Columns.Count; i++)
                {
                    table.Columns.Add(First.Columns[i].ColumnName, First.Columns[i].DataType);
                }
                //If First Row not in Second, Add to return table.
                table.BeginLoadData();
                foreach (DataRow parentrow in ds4.Tables[0].Rows)
                { 
                    DataRow[] childrows = parentrow.GetChildRows(r);

                    if (childrows == null || childrows.Length == 0)
                        table.LoadDataRow(parentrow.ItemArray, true);
                    table1.LoadDataRow(childrows, false);

                }
                table.EndLoadData();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        return table;
}
1
ashok

単にlinqを使用してそれを実現します。

private DataTable CompareDT(DataTable TableA, DataTable TableB)
    {
        DataTable TableC = new DataTable();
        try
        {

            var idsNotInB = TableA.AsEnumerable().Select(r => r.Field<string>(Keyfield))
            .Except(TableB.AsEnumerable().Select(r => r.Field<string>(Keyfield)));
            TableC = (from row in TableA.AsEnumerable()
                      join id in idsNotInB
                      on row.Field<string>(ddlColumn.SelectedItem.ToString()) equals id
                      select row).CopyToDataTable();
        }
        catch (Exception ex)
        {
            lblresult.Text = ex.Message;
            ex = null;
         }
        return TableC;

    }
0
SSJGSS
        try
        {
            if (ds.Tables[0].Columns.Count == ds1.Tables[0].Columns.Count)
            {
               for (int i = 0; i < ds.Tables[0].Rows.Count; i++)
               {
                    for (int j = 0; j < ds.Tables[0].Columns.Count; j++)
                   {
       if (ds.Tables[0].Rows[i][j].ToString() == ds1.Tables[0].Rows[i][j].ToString())
                       {


                        }
                        else
                        {

                           MessageBox.Show(i.ToString() + "," + j.ToString());


                       }

                                               }

                }

            }
            else
            {
               MessageBox.Show("Table has different columns ");
            }
        }
        catch (Exception)
        {
           MessageBox.Show("Please select The Table");
        }
0
ashok

Tzotのアイデアを続けています...

2つのソート可能なセットがある場合は、次のように使用できます。

List<string> diffList = new List<string>(sortedListA.Except(sortedListB));

より複雑なオブジェクトが必要な場合は、自分でコンパレータを定義して使用できます。

0
Ying

通常の使用シナリオでは、DataTableを手元に持っているユーザーを考慮し、DataRowsの一部を追加、削除、または変更して変更します。

変更が実行された後、DataTableは各行の適切なDataRowStateを認識し、またすべての行のOriginalDataRowVersionを追跡します変更されました。

この通常のシナリオでは、ソーステーブル(すべての行がMergeである)に変更をUnchanged戻すことができます。マージ後、GetChanges()を呼び出すことで、変更された行のみの素晴らしい要約を取得できます。

より珍しいシナリオでは、ユーザーは2つのDataTablesが同じスキーマ(またはおそらく同じ列のみで主キーがない)を持っています。これら2つのDataTablesUnchanged行のみで構成されています。ユーザーは、2つのテーブルの1つに適用するために、2つのテーブルの1つにどのような変更を加える必要があるかを知りたい場合があります。つまり、追加、削除、または変更する必要がある行。

ここで、仕事をするGetDelta()という関数を定義します。

using System;
using System.Data;
using System.Xml;
using System.Linq;
using System.Collections.Generic;
using System.Data.DataSetExtensions;

public class Program
{
    private static DataTable GetDelta(DataTable table1, DataTable table2)
    {
        // Modified2 : row1 keys match rowOther keys AND row1 does not match row2:
        IEnumerable<DataRow> modified2 = (
            from row1 in table1.AsEnumerable()
            from row2 in table2.AsEnumerable()
            where table1.PrimaryKey.Aggregate(true, (boolAggregate, keycol) => boolAggregate & row1[keycol].Equals(row2[keycol.Ordinal]))
                  && !row1.ItemArray.SequenceEqual(row2.ItemArray)
            select row2);

        // Modified1 :
        IEnumerable<DataRow> modified1 = (
            from row1 in table1.AsEnumerable()
            from row2 in table2.AsEnumerable()
            where table1.PrimaryKey.Aggregate(true, (boolAggregate, keycol) => boolAggregate & row1[keycol].Equals(row2[keycol.Ordinal]))
                  && !row1.ItemArray.SequenceEqual(row2.ItemArray)
            select row1);

        // Added : row2 not in table1 AND row2 not in modified2
        IEnumerable<DataRow> added = table2.AsEnumerable().Except(modified2, DataRowComparer.Default).Except(table1.AsEnumerable(), DataRowComparer.Default);

        // Deleted : row1 not in row2 AND row1 not in modified1
        IEnumerable<DataRow> deleted = table1.AsEnumerable().Except(modified1, DataRowComparer.Default).Except(table2.AsEnumerable(), DataRowComparer.Default);


        Console.WriteLine();
        Console.WriteLine("modified count =" + modified1.Count());
        Console.WriteLine("added count =" + added.Count());
        Console.WriteLine("deleted count =" + deleted.Count());

        DataTable deltas = table1.Clone();

        foreach (DataRow row in modified2)
        {
            // Match the unmodified version of the row via the PrimaryKey
            DataRow matchIn1 = modified1.Where(row1 =>  table1.PrimaryKey.Aggregate(true, (boolAggregate, keycol) => boolAggregate & row1[keycol].Equals(row[keycol.Ordinal]))).First();
            DataRow newRow = deltas.NewRow();

            // Set the row with the original values
            foreach(DataColumn dc in deltas.Columns)
                newRow[dc.ColumnName] = matchIn1[dc.ColumnName];
            deltas.Rows.Add(newRow);
            newRow.AcceptChanges();

            // Set the modified values
            foreach (DataColumn dc in deltas.Columns)
                newRow[dc.ColumnName] = row[dc.ColumnName];
            // At this point newRow.DataRowState should be : Modified
        }

        foreach (DataRow row in added)
        {
            DataRow newRow = deltas.NewRow();
            foreach (DataColumn dc in deltas.Columns)
                newRow[dc.ColumnName] = row[dc.ColumnName];
            deltas.Rows.Add(newRow);
            // At this point newRow.DataRowState should be : Added
        }


        foreach (DataRow row in deleted)
        {
            DataRow newRow = deltas.NewRow();
            foreach (DataColumn dc in deltas.Columns)
                newRow[dc.ColumnName] = row[dc.ColumnName];
            deltas.Rows.Add(newRow);
            newRow.AcceptChanges();
            newRow.Delete();
            // At this point newRow.DataRowState should be : Deleted
        }

        return deltas;
    }

    private static void DemonstrateGetDelta()
    {
        DataTable table1 = new DataTable("Items");

        // Add columns
        DataColumn column1 = new DataColumn("id1", typeof(System.Int32));
        DataColumn column2 = new DataColumn("id2", typeof(System.Int32));
        DataColumn column3 = new DataColumn("item", typeof(System.Int32));
        table1.Columns.Add(column1);
        table1.Columns.Add(column2);
        table1.Columns.Add(column3);

        // Set the primary key column.
        table1.PrimaryKey = new DataColumn[] { column1, column2 };


        // Add some rows.
        DataRow row;
        for (int i = 0; i <= 4; i++)
        {
            row = table1.NewRow();
            row["id1"] = i;
            row["id2"] = i*i;
            row["item"] = i;
            table1.Rows.Add(row);
        }

        // Accept changes.
        table1.AcceptChanges();
        PrintValues(table1, "table1:");

        // Create a second DataTable identical to the first.
        DataTable table2 = table1.Clone();

        // Add a row that exists in table1:
        row = table2.NewRow();
        row["id1"] = 0;
        row["id2"] = 0; 
        row["item"] = 0;
        table2.Rows.Add(row);

        // Modify the values of a row that exists in table1:
        row = table2.NewRow();
        row["id1"] = 1;
        row["id2"] = 1;
        row["item"] = 455;
        table2.Rows.Add(row);

        // Modify the values of a row that exists in table1:
        row = table2.NewRow();
        row["id1"] = 2;
        row["id2"] = 4;
        row["item"] = 555;
        table2.Rows.Add(row);

        // Add a row that does not exist in table1:
        row = table2.NewRow();
        row["id1"] = 13;
        row["id2"] = 169;
        row["item"] = 655;
        table2.Rows.Add(row);

        table2.AcceptChanges();

        Console.WriteLine();
        PrintValues(table2, "table2:");

        DataTable delta = GetDelta(table1,table2);

        Console.WriteLine();
        PrintValues(delta,"delta:");

        // Verify that the deltas DataTable contains the adequate Original DataRowVersions:
        DataTable originals = table1.Clone();
        foreach (DataRow drow in delta.Rows)
        {
            if (drow.RowState != DataRowState.Added)
            {
                DataRow originalRow = originals.NewRow();
                foreach (DataColumn dc in originals.Columns)
                    originalRow[dc.ColumnName] = drow[dc.ColumnName, DataRowVersion.Original];
                originals.Rows.Add(originalRow);
            }
        }
        originals.AcceptChanges();

        Console.WriteLine();
        PrintValues(originals,"delta original values:");
    }

    private static void Row_Changed(object sender, 
        DataRowChangeEventArgs e)
    {
        Console.WriteLine("Row changed {0}\t{1}", 
            e.Action, e.Row.ItemArray[0]);
    }

    private static void PrintValues(DataTable table, string label)
    {
        // Display the values in the supplied DataTable:
        Console.WriteLine(label);
        foreach (DataRow row in table.Rows)
        {
            foreach (DataColumn col in table.Columns)
            {
                Console.Write("\t " + row[col, row.RowState == DataRowState.Deleted ? DataRowVersion.Original : DataRowVersion.Current].ToString());
            }
            Console.Write("\t DataRowState =" + row.RowState);
            Console.WriteLine();
        }
    }

    public static void Main()
    {
        DemonstrateGetDelta();
    }
}

上記のコードは https://dotnetfiddle.net/ でテストできます。結果の出力を以下に示します。

table1:
     0     0     0     DataRowState =Unchanged
     1     1     1     DataRowState =Unchanged
     2     4     2     DataRowState =Unchanged
     3     9     3     DataRowState =Unchanged
     4     16     4     DataRowState =Unchanged

table2:
     0     0     0     DataRowState =Unchanged
     1     1     455     DataRowState =Unchanged
     2     4     555     DataRowState =Unchanged
     13     169     655     DataRowState =Unchanged

modified count =2
added count =1
deleted count =2

delta:
     1     1     455     DataRowState =Modified
     2     4     555     DataRowState =Modified
     13     169     655     DataRowState =Added
     3     9     3     DataRowState =Deleted
     4     16     4     DataRowState =Deleted

delta original values:
     1     1     1     DataRowState =Unchanged
     2     4     2     DataRowState =Unchanged
     3     9     3     DataRowState =Unchanged
     4     16     4     DataRowState =Unchanged

テーブルにPrimaryKeyがない場合は、LINQクエリのwhere句が少し簡略化されることに注意してください。私はあなたが自分でそれを理解できるようにします。

0
Pedro M Duarte