Я ищу способ улучшить производительность кода С# и буду признателен за любую помощь.
Есть 2 таблицы: Table_1 и Table_2, и я хочу собрать данные из Table_2 и сохранить в Table_1 в следующем виде:
Таблица 1
Дата | Some_Stat |
---|---|
2021-06-01 | 23,7 |
2021-06-02 | 12,6 |
2021-06-03 | 47,9 |
Таблица 2
Дата | Я БЫ | А | Б | С |
---|---|---|---|---|
2021-06-02 | 4 | 21 | 23 | 13 |
2021-06-02 | 3 | 67 | 31 | 25 |
2021-06-01 | 3 | 45 | 54 | 33 |
2021-06-03 | 3 | 71 | 28 | 51 |
2021-06-03 | 4 | 26 | 83 | 24 |
Моя цель: Таблица_1 после присоединения
Дата | Some_Stat | А_3 | Б_3 | С_3 | А_4 | Б_4 | С_4 |
---|---|---|---|---|---|---|---|
2021-06-01 | 23,7 | 45 | 54 | 33 | 0 | 0 | 0 |
2021-06-02 | 12,6 | 67 | 31 | 25 | 21 | 23 | 13 |
2021-06-03 | 47,9 | 71 | 28 | 51 | 26 | 83 | 24 |
Чтобы добиться этого, я использовал приведенный ниже код, который работает, но очень медленно, особенно при наличии тысяч идентификаторов. По сути, код делает сначала необходимое соединение и передает результаты соединения LINQ в существующую таблицу Table_1 (копирует столбцы). Я проверил время производительности, и запрос LINQ всегда очень быстрый (время: 0 мс), но проблема заключается в передаче данных.
List<int> ids = Table_2.AsEnumerable().Select(s => s.Field<int>("ID").Distinct().ToList();
for (int i = 0; ind < ids.Count; i++)
{
Table_1.Columns.Add($"A_{ids[i]}", typeof(double));
Table_1.Columns.Add($"B_{ids[i]}", typeof(double));
Table_1.Columns.Add($"C_{ids[i]}", typeof(double));
}
for (int i = 0; ind < ids.Count; i++)
{
// LINQ join (fast)
var joinedTables = from T1 in Table_1.AsEnumerable()
join T2 in Table_2.Select($"ID = {ids[i]}").AsEnumerable()
on (String)T1["Date"] equals (String)T2["Date"]
into T1_and_T2
from TT in T1_and_T2.DefaultIfEmpty()
select new
{
Date = (String)T1["Date"],
A = TT != null ? (double)TT["A"] : 0.0,
B = TT != null ? (double)TT["B"] : 0.0,
C = TT != null ? (double)TT["C"] : 0.0,
};
// data transfer (very slow)
for (int day = 0; day < joinedTables.Count(); day++)
{
Table_1.Rows[day][$"A_{ids[i]}"] = joinedTables.ElementAt(day).A;
Table_1.Rows[day][$"B_{ids[i]}"] = joinedTables.ElementAt(day).B;
Table_1.Rows[day][$"C_{ids[i]}"] = joinedTables.ElementAt(day).C;
}
}
Также вместо версии передачи данных, указанной выше, я попробовал другой способ, но он такой же медленный, как и предыдущий:
int day = 0;
foreach( var row in joinedTables)
{
Table_1.Rows[day][$"A_{ids[i]}"] = row.A;
Table_1.Rows[day][$"B_{ids[i]}"] = row.B;
Table_1.Rows[day][$"C_{ids[i]}"] = row.C;
}
Я также открыт для новых подходов к сбору данных из Table_2
в Table_1
. Может быть способ использовать встроенные функции (написанные на C или C++), которые будут напрямую обращаться к машинному коду (например, функция, которая копирует столбцы из одной таблицы в другую, как в python), чтобы избежать зацикливания. ряды.
Примечание. В исходной задаче Table_1
может иметь до 500 строк и 10 столбцов, а Table_2
может иметь до 5000 строк в день и заполняться от 50 до 100%. Приведенные выше решения в крайнем случае займут от 2 до 4 часов, что очень медленно (думаю, это можно сделать за считанные минуты).
Обратите также внимание, что T1_and_T2
это не так; это просто соединенный T2
(я бы назвал его T2j
). В нем нет ничего из T1
. Тогда вашим следующим from
будет from T2 in T2j.DefaultIfEmpty()
.
большое спасибо, все работает, но так же медленно, как и с ElementAt()
Это должно быть намного быстрее, чем ElementAt
, но я решил, что лучше использовать совершенно другой подход, поэтому я опубликовал это как свой ответ.
Я бы предложил включить в вопрос некоторые конкретные показатели размеров связанных таблиц (Table_1
и Table_2
), включая фактическое количество столбцов Table_1
. Без этой информации любому, кто читает этот вопрос, очень сложно догадаться, в чем может быть проблема. В идеале вы должны включить минимальный воспроизводимый пример, демонстрирующий проблемное поведение (включая конкретные измерения времени). Это значительно улучшит вопрос и даст ему право на голосование.
Вы правы @TheodorZoulias, я так и сделаю
Вы можете использовать оператор ToLookup
LINQ и создать эффективную структуру Lookup<DateTime, DataRow>
из содержимого файла Table_2
. Эта структура только для чтения будет содержать один IGrouping<DateTime, DataRow>
для каждой уникальной даты, и каждая группа будет содержать все DataRow
, связанные с этой датой:
var lookup = Table_2.AsEnumerable().ToLookup(r => r.Field<DateTime>("Date"));
Тогда для каждой строки Table_1
вы сможете быстро найти все связанные строки Table_2
:
foreach (DataRow row1 in Table_1.Rows)
{
DateTime date = row1.Field<DateTime>("Date");
IEnumerable<DataRow> group = lookup[date];
if (group == null) continue;
foreach (DataRow row2 in group)
{
int id = row2.Field<int>("ID");
row1[$"A_{id}"] = row2.Field<double>("A");
row1[$"B_{id}"] = row2.Field<double>("B");
row1[$"C_{id}"] = row2.Field<double>("C");
}
}
Обновлять: Похоже, ваша проблема с производительностью связана не с объединением двух DataTable
, а с обновлением чрезвычайно широкого DataTable
, содержащего сотни или даже тысячи DataColumn
. Очевидно, DataTable
не оптимизированы для таких сценариев. Сложность обновления любого столбца DataRow
составляет O (n²), где N — общее количество столбцов. Чтобы решить эту проблему, вы можете экспортировать все значения, хранящиеся в DataRow
, через свойство ItemArray
, манипулировать этими значениями и, наконец, импортировать их обратно через то же свойство. Производительность можно улучшить еще больше, используя методы BeginLoadData
и EndLoadData
.
Table_1.BeginLoadData();
foreach (DataRow row1 in Table_1.Rows)
{
DateTime date = row1.Field<DateTime>("Date");
IEnumerable<DataRow> group = lookup[date];
if (group == null) continue;
object[] values = row1.ItemArray; // Export the raw data
foreach (DataRow row2 in group)
{
int id = row2.Field<int>("ID");
values[Table_1.Columns[$"A_{id}"].Ordinal] = row2.Field<double>("A");
values[Table_1.Columns[$"B_{id}"].Ordinal] = row2.Field<double>("B");
values[Table_1.Columns[$"C_{id}"].Ordinal] = row2.Field<double>("C");
}
row1.ItemArray = values; // Import the updated data
}
Table_1.EndLoadData();
Красиво, спасибо!! Я попробовал это, и это также работает, однако всего на 10% быстрее, я подумал, что может быть способ использовать некоторые встроенные функции (например, копирование столбцов из одной таблицы в другую), которые написаны на C или C++, в для прямого доступа к машинному коду (например, в python)
@ klop52 хм, я ожидаю, что это будет довольно быстро. Не могли бы вы измерить время с помощью Stopwatch
, запустить его непосредственно перед ToLookup
и остановить сразу после внешнего цикла foreach
и сообщить ElapsedMilliseconds
? Я подозреваю, что есть что-то еще, что медленно, до или после, и влияет на общую скорость.
Время выполнения для подготовки данных со старой версией: 0ч 0м 10с 358мс ; Время выполнения для подготовки данных с обновленной версией: 0ч 0м 9с 83мс.
@ klop52 сколько строк (приблизительно) в таблицах Table_1
и Table_2
?
Я пробую это сейчас в течение 23 дней и около 430 идентификаторов.
Спасибо за помощь, на днях попробую
Я рекомендую использовать другой подход. DataTable
не особенно быстрый объект, и поиск столбцов для установки значений происходит медленно. Создание нового DataTable
для замены Table_1
может быть намного быстрее, поскольку вы можете использовать метод DataRowCollection.Add()
для быстрого добавления строк.
Использование Dictionary
для преобразования Table_2
также обеспечивает гораздо более быстрый поиск, чем ElementAt
.
var joinDict = (from T2 in Table_2.AsEnumerable()
select new {
Date = T2.Field<string>("Date"),
ID = T2.Field<int>("ID"),
A = T2.Field<double>("A"),
B = T2.Field<double>("B"),
C = T2.Field<double>("C"),
})
.ToDictionary(t2 => (t2.Date, t2.ID));
List<int> ids = Table_2.AsEnumerable().Select(s => s.Field<int>("ID")).Distinct().OrderBy(x => x).ToList();
var ans = Table_1.Clone();
for (int i = 0; i < ids.Count; i++) {
ans.Columns.Add($"A_{ids[i]}", typeof(double));
ans.Columns.Add($"B_{ids[i]}", typeof(double));
ans.Columns.Add($"C_{ids[i]}", typeof(double));
}
foreach (DataRow row in Table_1.Rows) {
var newRow = new List<object> { row.Field<string>("Date") };
foreach (var id in ids) {
if (joinDict.TryGetValue((row.Field<string>("Date"), id), out var t2))
newRow.AddRange(new object[] { t2.A, t2.B, t2.C });
else
newRow.AddRange(new object[] { 0.0, 0.0, 0.0 });
}
ans.Rows.Add(newRow.ToArray());
}
Table_1 = ans;
При тестировании со 100 днями в Table_1
и 500 строками в день в Table_2
75% населения я получаю ускорение примерно в 128 раз.
вау, это так здорово, кажется, все работает, как вы говорите, спасибо @NetMage. Теперь мне просто нужно понять, как выполнить цикл for для этой таблицы и внести другие незначительные изменения...
Я не был уверен, стоит ли мне открывать новую тему, у меня есть короткий вопрос: как правильно зациклиться на Table_1? (Я просто борюсь с этим и не могу найти никакого способа, любая ссылка или подсказка будут оценены)
@ klop52 Зависит от того, почему вы зацикливаетесь? Что вы пытаетесь достичь? Самый простой — это foreach (DataRow dr in Table_1.Rows)
или foreach (var dr in Table_1.AsEnumerable())
, как добавление цикла в ответе.
Иногда мне также нужно сравнить значения между строками, поэтому цикл for был бы уместен, но когда я пытаюсь сделать это с помощью: Table_1.Rows[i]["A"] == 40 , это не работает. Есть ли способ также подать заявку на цикл? Поэтому я хочу одновременно получить доступ к определенным строкам и определенным столбцам и сохранить сумму в переменной. Я также пробовал: double S = 0; S = double.Parse(Table_1.Rows[i]["A"].ToString()) + double.Parse(Table_1.Rows[i]["B"].ToString()); , но это принесло мне ошибку ввода
@ klop52 Не должно быть столбца «A» - все они «A_#» на основе «ID». Кроме того, всегда используйте Field<T>("column name")
вместо []
, чтобы убедиться, что ваш ==
использует правильные типы.
А, правда, извините. Спасибо за помощь! Я попробую
Ваша проблема заключается в фундаментальном непонимании LINQ. Присваивание
joinTables
не выполняет соединение, оно создает структуру данных, которая представляет вычисление соединения. Когда вы вызываетеElementAt
, он запускает соединение, затем пропускает результат, чтобы найти элемент. ИзменитеjoinTables
наjoinList
, поместите LINQ в()
и поставьте.ToList()
в конце. ЗаменитеElementAt(day)
на[day]
.