Сравните две таблицы данных для определения строк в одном, но не другом
У меня есть два DataTables, A
и B
, созданные из файлов CSV. Мне нужно проверить, какие строки существуют в B
, которые не существуют в A
.
Есть ли способ сделать какой-то запрос, чтобы показать разные строки, или мне придется перебирать каждую строку на каждом DataTable, чтобы проверить, совпадают ли они? Последний вариант кажется очень интенсивным, если таблицы становятся большими.
Ответы
Ответ 1
мне нужно будет перебирать каждую строку на каждом DataTable, чтобы проверить, совпадают ли они.
Увидев, как вы загрузили данные из файла CSV, у вас не будет никаких индексов или чего-то еще, поэтому в какой-то момент что-то придется перебирать по каждой строке, будь то ваш код, или библиотеки, или что-то еще.
В любом случае, это вопрос с алгоритмами, который не является моей специальностью, но мой наивный подход был бы следующим:
1: Можете ли вы использовать какие-либо свойства данных? Все ли строки в каждой таблице уникальны, и можете ли вы отсортировать их по одинаковым критериям? Если это так, вы можете сделать это:
- Сортируйте обе таблицы по их идентификатору (используя некоторую полезную вещь, такую как quicksort). Если они уже отсортированы, вы выиграете большой.
- Пройдите через обе таблицы одновременно, пропуская пробелы в ID в любой таблице. Согласованный идентификатор означает дублированные записи.
Это позволяет вам сделать это (время сортировки * 2) + один проход, поэтому, если моя большая O-нотация правильная, это будет (независимо от времени сортировки) + O (m + n), которое довольно хорошо.
(Редакция: это подход, который описывает ΤΖΩΤΖΙΟΥ)
2: Альтернативный подход, который может быть более или менее эффективным в зависимости от того, насколько велики ваши данные:
- Запустите таблицу 1 и для каждой строки вставьте этот идентификатор (или вычисленный хэш-код или какой-либо другой уникальный идентификатор для этой строки) в словарь (или хэш-таблицу, если вы предпочитаете называть это).
- Запустите таблицу 2, и для каждой строки посмотрите, есть ли идентификатор (или хэш-код и т.д.) в словаре. Вы используете тот факт, что словари действительно быстрые - O (1) Я думаю? Погляди. Этот шаг будет очень быстрым, но вы заплатите цену за все эти словарные вставки.
Мне было бы очень интересно узнать, что люди с лучшими знаниями алгоритмов, чем я, придумали для этого: -)
Ответ 2
Предполагая, что у вас есть столбец идентификатора, который имеет соответствующий тип (т.е. дает хэш-код и реализует равенство) - строка в этом примере, которая немного псевдокода, потому что я не знаком с 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);
Ответ 3
Вы можете использовать методы Merge и GetChanges в DataTable для этого:
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
Ответ 4
До сих пор ответы предполагают, что вы просто ищете дублирующие первичные ключи. Это довольно простая проблема - вы можете использовать метод Merge(), например.
Но я понимаю, что ваш вопрос означает, что вы ищете дубликаты DataRows. (Из вашего описания проблемы, когда обе таблицы импортируются из файлов CSV, я бы даже предположил, что исходные строки не имеют значений первичного ключа и что любые первичные ключи назначаются через AutoNumber во время импорта.)
Наивная реализация (для каждой строки в A, сравните ее ItemArray с той, что у каждой строки в B) действительно будет дорого вычислительной.
Менее дорогой способ сделать это - с помощью алгоритма хеширования. Для каждого DataRow объединяйте строковые значения его столбцов в одну строку, а затем вызывайте GetHashCode() в этой строке, чтобы получить значение int. Создайте Dictionary<int, DataRow>
, который содержит запись с ключом хеш-кода для каждого DataRow в DataTable B. Затем для каждого DataRow в DataTable A вычислите хэш-код и посмотрите, содержится ли он в словаре. Если это не так, вы знаете, что DataRow не существует в DataTable B.
Этот подход имеет две слабости, которые возникают из того факта, что две строки могут быть неравными, но порождают один и тот же хэш-код. Если вы найдете строку в A, чей хэш находится в словаре, вам необходимо проверить DataRow в словаре, чтобы убедиться, что две строки действительно равны.
Вторая слабость более серьезная: маловероятно, но возможно, что два разных DataRows в B могут хешировать до одного и того же ключевого значения. По этой причине словарь действительно должен быть Dictionary<int, List<DataRow>>
, и вы должны выполнить проверку, описанную в предыдущем абзаце, против каждого DataRow в списке.
Для получения этой работы требуется немалая работа, но это алгоритм O (m + n), который, я думаю, будет таким же хорошим, как и он.
Ответ 5
Просто FYI:
В общем случае об алгоритмах сравнение двух наборов сортируемых (как правило, это идентификаторы) не является операцией O (M * N/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
Приведенный выше код явно псевдокод, но должен дать вам общий смысл, если вы решите сам его закодировать.
Ответ 6
Спасибо за отзывы.
У меня нет никакого индекса, к сожалению. Я дам немного больше информации о своей ситуации.
У нас есть программа отчетности (заменена отчеты Crystal), которая установлена на 7 серверах по всему ЕС. На этих серверах есть много сообщений о них (не все одинаково для каждой страны). Они вызываются приложением командной строки, которое использует XML файлы для их конфигурации. Таким образом, один файл XML может вызывать несколько отчетов.
Приложение командной строки запланировано и контролируется нашим ночным процессом. Таким образом, XML файл можно было бы вызывать из нескольких мест.
Цель CSV - создать список всех отчетов, которые используются и откуда они вызывают.
Я просматриваю файлы XML для всех ссылок, запрашивая программу планирования и составляя список всех отчетов. (это не так уж плохо).
Проблема, которую я имею, - это сохранить список всех отчетов, которые могли быть удалены из производства. Поэтому мне нужно сравнить старый CSV с новыми данными. Для этого я подумал, что лучше всего помещать его в DataTables и сравнивать информацию (это может быть неправильный подход. Я полагаю, что я мог бы создать объект, который его удерживает, и сравнивает разницу, а затем создает итерацию через них).
Данные о каждом отчете следующие:
String - Имя задачи
Строка - название действия
Int-ActionID (идентификатор действия может быть в нескольких записях, поскольку одно действие может вызывать множество отчетов, то есть XML файл).
String - XML файл, называемый
Строка - название отчета
Я попробую идею слияния, данную MusiGenesis (спасибо). (перечитывая, что некоторые из сообщений не уверены, что Merge будет работать, но стоит попробовать, поскольку я не слышал об этом, прежде чем что-то новое, чтобы узнать).
Идея HashCode также интересна.
Спасибо за все советы.
Ответ 7
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;
}
Ответ 8
Я нашел простой способ решить эту проблему. В отличие от предыдущих ответов "за исключением метода", я дважды использую метод except. Это не только говорит о том, какие строки были удалены, но какие строки были добавлены. Если вы используете только один метод - он скажет только одно различие, а не то, и другое. Этот код проверен и работает. См. Ниже
//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
}
}
Ответ 9
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");
}
Ответ 10
Я продолжаю идею tzot...
Если у вас есть два сортируемых набора, вы можете просто использовать:
List<string> diffList = new List<string>(sortedListA.Except(sortedListB));
Если вам нужны более сложные объекты, вы можете сами определить компаратор и все еще использовать его.
Ответ 11
В обычном сценарии использования учитывается пользователь с DataTable
и изменяет его, добавляя, удаляя или изменяя некоторые из DataRows
.
После того, как изменения выполнены, DataTable
знает о надлежащем DataRowState
для каждой строки, а также отслеживает Original
DataRowVersion
для любых строк, которые были изменены.
В этом обычном сценарии Merge
можно вернуться к исходной таблице (в которой все строки Unchanged
). После слияния можно получить хорошее резюме только измененных строк с вызовом GetChanges()
.
В более необычном сценарии у пользователя есть два DataTables
с той же схемой (или, возможно, только с теми же столбцами и без первичных ключей). Эти два DataTables
состоят только из строк Unchanged
. Пользователь может захотеть выяснить, какие изменения необходимо применить к одной из двух таблиц, чтобы перейти к другой. То есть, какие строки должны быть добавлены, удалены или изменены.
Определим здесь функцию, называемую 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
, предложение where
в запросах LINQ немного упрощается. Я позволю тебе понять это самостоятельно.
Ответ 12
Достичь его просто используя 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;
}
Ответ 13
Не могли бы вы просто сравнить файлы CSV перед их загрузкой в 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 ...