web-dev-qa-db-fra.com

Comparez deux DataTables pour déterminer les lignes dans l'une mais pas l'autre

J'ai deux tables de données, A et B, produites à partir de fichiers CSV. Je dois pouvoir vérifier quelles lignes existent dans B et qui n'existent pas dans A.

Y at-il un moyen de faire une sorte de requête pour afficher les différentes lignes ou devrais-je parcourir chaque ligne sur chaque DataTable pour vérifier si elles sont les mêmes? Cette dernière option semble être très intensive si les tables deviennent grandes.

17
Jon

aurais-je à parcourir chaque ligne de chaque DataTable pour vérifier si elles sont identiques?.

Étant donné que vous avez chargé les données à partir d'un fichier CSV, vous ne disposerez d'aucun index ni quoi que ce soit, de sorte qu'à un moment donné, il sera nécessaire de parcourir toutes les lignes, que ce soit votre code ou une bibliothèque. , ou peu importe.

Quoi qu'il en soit, ceci est une question d'algorithmes, qui n'est pas ma spécialité, mais mon approche naïve serait la suivante:

1: Pouvez-vous exploiter les propriétés des données? Toutes les lignes de chaque table sont-elles uniques et pouvez-vous les trier selon les mêmes critères? Si oui, vous pouvez faire ceci:

  • Triez les deux tables par leur ID (en utilisant quelque chose de utile, comme un tri rapide). S'ils sont déjà triés, vous gagnez gros.
  • Parcourez les deux tables en même temps, en ignorant les lacunes des identifiants dans les deux tables. Appariés signifient les enregistrements dupliqués.

Cela vous permet de le faire en (temps de tri * 2) + un passage, donc si mon gros-O-notation est correcte, ce serait (quel que soit le temps de tri) + O (m + n) ce qui est plutôt bon .
(Révision: il s’agit de l’approche décrite par ΤΖΙΟΥΩΤΖΙΟΥ )

2: Une approche alternative, qui peut être plus ou moins efficace en fonction de la taille de vos données:

  • Parcourez le tableau 1 et, pour chaque ligne, collez son ID (ou le hashcode calculé, ou un autre ID unique pour cette ligne) dans un dictionnaire (ou une table de hachage si vous préférez l'appeler ainsi).
  • Parcourez le tableau 2 et voyez pour chaque ligne si l'ID (ou le hashcode, etc.) est présent dans le dictionnaire. Vous exploitez le fait que les dictionnaires ont vraiment rapide - O(1) Je pense? Chercher. Cette étape sera très rapide, mais vous aurez payé le prix en faisant toutes ces insertions dans le dictionnaire.

Je serais vraiment intéressé de voir ce que les gens ayant une meilleure connaissance des algorithmes que moi proposent pour celui-ci :-)

9
Orion Edwards

En supposant que vous ayez une colonne d'ID d'un type approprié (c'est-à-dire, qui donne un hashcode et implémente l'égalité) - string dans cet exemple, qui est légèrement pseudocode car je ne suis pas familier avec DataTables et je n'ai pas le temps de tout regarder debout tout à l'heure :)

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);
19
Jon Skeet

Vous pouvez utiliser les méthodes Merge et GetChanges sur le DataTable pour cela:

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

Jusqu'à présent, les réponses supposent que vous recherchez simplement des clés primaires en double. C'est un problème assez facile - vous pouvez utiliser la méthode Merge (), par exemple.

Mais je comprends que votre question signifie que vous recherchez des DataRows en double. (D'après votre description du problème, les deux tables étant importées à partir de fichiers CSV, je suppose même que les lignes d'origine ne comportaient pas de valeurs de clé primaire et que les clés primaires étaient attribuées via AutoNumber lors de l'importation.)

L'implémentation naïve (pour chaque ligne dans A, comparez son ItemArray avec celui de chaque ligne dans B) va effectivement être coûteuse en calcul.

Une méthode beaucoup moins coûteuse consiste à utiliser un algorithme de hachage. Pour chaque DataRow, concaténez les valeurs de chaîne de ses colonnes en une seule chaîne, puis appelez GetHashCode () sur cette chaîne pour obtenir une valeur int. Créez un Dictionary<int, DataRow> contenant une entrée associée au code de hachage pour chaque DataRow de DataTable B. Ensuite, calculez le code de hachage pour chaque DataRow de DataTable A et voyez s'il est contenu dans le dictionnaire. Si ce n'est pas le cas, vous savez que le DataRow n'existe pas dans DataTable B.

Cette approche présente deux faiblesses qui résultent du fait que deux chaînes peuvent être inégales mais produisent le même code de hachage. Si vous trouvez une ligne dans A dont le hachage est dans le dictionnaire, vous devez alors vérifier le DataRow dans le dictionnaire pour vérifier que les deux lignes sont vraiment égales. 

La deuxième faiblesse est plus grave: il est improbable, mais possible, que deux DataRows différents dans B puissent avoir la même valeur de clé. Pour cette raison, le dictionnaire doit être réellement un Dictionary<int, List<DataRow>> et vous devez effectuer la vérification décrite dans le paragraphe précédent sur chaque DataRow de la liste.

Pour que cela fonctionne, il faut beaucoup de travail, mais il s’agit d’un algorithme O (m + n), qui, à mon avis, sera aussi performant que possible.

4
Robert Rossney

Ne pourriez-vous pas simplement comparer les fichiers CSV avant les charger dans 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

J'ai trouvé un moyen facile de résoudre ce problème. Contrairement aux réponses précédentes "except method", j'utilise la méthode except deux fois. Cela vous indique non seulement quelles lignes ont été supprimées mais également quelles lignes ont été ajoutées. Si vous utilisez une seule méthode sauf - cela ne vous indiquera qu'une différence et non les deux. Ce code est testé et fonctionne. Voir ci-dessous

//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);

puis exécutez votre code contre les résultats

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


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



        }
1
NewCsharper

Juste FYI:

En règle générale, en ce qui concerne les algorithmes, comparer deux ensembles d’objets triables (comme le sont généralement les ids) n’est pas une opération O (M * N/2), mais O (M + N) si les deux ensembles sont ordonnés. Donc, vous analysez une table avec un pointeur au début de l’autre, et:

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

Le code ci-dessus est évidemment pseudocode, mais devrait vous donner le Gist général si vous décidez de le coder vous-même.

1
tzot

Merci pour tous les commentaires.

Je n'ai malheureusement pas d'index. Je vais donner un peu plus d'informations sur ma situation.

Nous avons un programme de génération de rapports (Crystal Reports) qui est installé sur 7 serveurs dans l’UE. Ces serveurs ont beaucoup de rapports sur eux (pas tous les mêmes pour chaque pays). Ils sont appelés par une application en ligne de commande qui utilise des fichiers XML pour leur configuration. Donc, un fichier XML peut appeler plusieurs rapports.

L'application en ligne de commande est planifiée et contrôlée par notre processus de nuit. Ainsi, le fichier XML peut être appelé à partir de plusieurs endroits.

L'objectif du CSV est de produire une liste de tous les rapports utilisés et de l'endroit où ils sont appelés.

Je consulte les fichiers XML pour toutes les références, interroge le programme de planification et crée une liste de tous les rapports. (c'est pas trop mal).

Le problème est que je dois garder une liste de tous les rapports qui auraient pu être retirés de la production. J'ai donc besoin de comparer l'ancien CSV avec les nouvelles données. Pour cela, j'ai pensé qu'il était préférable de le mettre dans DataTables et de comparer les informations (cela pourrait être une mauvaise approche. Je suppose que je pourrais créer un objet qui le retient et compare la différence puis crée une itération à travers elles.

Les données que j'ai sur chaque rapport sont les suivantes:

String - Nom de la tâche String - Nom de l'actionInt - ActionID (l'ID de l'action peut figurer dans plusieurs enregistrements, une seule action pouvant appeler plusieurs rapports, c'est-à-dire un fichier XML). String - Fichier XML appelé String - Nom du rapport

Je vais essayer l'idée de fusion donnée par MusiGenesis (merci). (relisant certains des articles ne sachant pas si la fusion fonctionnera, mais cela vaut la peine d’essayer car je n’en ai pas entendu parler auparavant, alors il ya quelque chose de nouveau à apprendre).

L'idée HashCode semble également intéressante.

Merci pour tous les conseils.

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

Atteindre simplement en utilisant 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

Je continue l'idée de tzot ...

Si vous avez deux ensembles triables, vous pouvez simplement utiliser:

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

Si vous avez besoin d’objets plus complexes, vous pouvez définir vous-même un comparateur et l’utiliser quand même.

0
Ying

Le scénario d'utilisation habituel considère un utilisateur qui a une DataTable en main et le modifie en ajoutant, supprimant ou modifiant certaines des DataRows

Une fois les modifications effectuées, la DataTable connaît la DataRowState appropriée pour chaque ligne et conserve également la trace de la OriginalDataRowVersion pour toutes les lignes modifiées. 

Dans ce scénario habituel, on peut Merge les modifications dans une table source (dans laquelle toutes les lignes sont Unchanged). Après la fusion, on peut obtenir un résumé Nice des seules lignes modifiées avec un appel à GetChanges()

Dans un scénario plus inhabituel, un utilisateur a deux DataTables avec le même schéma (ou peut-être uniquement les mêmes colonnes et les clés primaires manquantes). Ces deux DataTables se composent de seulement Unchanged rangées. L'utilisateur peut vouloir savoir quelles modifications il doit appliquer à l'une des deux tables pour accéder à l'autre. C'est-à-dire quelles lignes doivent être ajoutées, supprimées ou modifiées. 

Nous définissons ici une fonction appelée GetDelta() qui fait le travail:

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();
    }
}

Le code ci-dessus peut être testé dans https://dotnetfiddle.net/ . Le résultat obtenu est présenté ci-dessous:

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

Notez que si vos tables n'ont pas de PrimaryKey, la clause where dans les requêtes LINQ est un peu simplifiée. Je vous laisse le découvrir vous-même. 

0
Pedro M Duarte