Скопируйте результаты запроса LINQ в существующую таблицу данных с более высокой производительностью

Я ищу способ улучшить производительность кода С# и буду признателен за любую помощь.

Есть 2 таблицы: Table_1 и Table_2, и я хочу собрать данные из Table_2 и сохранить в Table_1 в следующем виде:

Таблица 1

ДатаSome_Stat
2021-06-0123,7
2021-06-0212,6
2021-06-0347,9

Таблица 2

ДатаЯ БЫАБС
2021-06-024212313
2021-06-023673125
2021-06-013455433
2021-06-033712851
2021-06-034268324

Моя цель: Таблица_1 после присоединения

ДатаSome_StatА_3Б_3С_3А_4Б_4С_4
2021-06-0123,7455433000
2021-06-0212,6673125212313
2021-06-0347,9712851268324

Чтобы добиться этого, я использовал приведенный ниже код, который работает, но очень медленно, особенно при наличии тысяч идентификаторов. По сути, код делает сначала необходимое соединение и передает результаты соединения 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 часов, что очень медленно (думаю, это можно сделать за считанные минуты).

Ваша проблема заключается в фундаментальном непонимании LINQ. Присваивание joinTables не выполняет соединение, оно создает структуру данных, которая представляет вычисление соединения. Когда вы вызываете ElementAt, он запускает соединение, затем пропускает результат, чтобы найти элемент. Измените joinTables на joinList, поместите LINQ в () и поставьте .ToList() в конце. Замените ElementAt(day) на [day].

NetMage 17.03.2022 22:48

Обратите также внимание, что T1_and_T2 это не так; это просто соединенный T2 (я бы назвал его T2j). В нем нет ничего из T1. Тогда вашим следующим from будет from T2 in T2j.DefaultIfEmpty().

NetMage 17.03.2022 23:00

большое спасибо, все работает, но так же медленно, как и с ElementAt()

klop52 18.03.2022 11:30

Это должно быть намного быстрее, чем ElementAt, но я решил, что лучше использовать совершенно другой подход, поэтому я опубликовал это как свой ответ.

NetMage 18.03.2022 17:08

Я бы предложил включить в вопрос некоторые конкретные показатели размеров связанных таблиц (Table_1 и Table_2), включая фактическое количество столбцов Table_1. Без этой информации любому, кто читает этот вопрос, очень сложно догадаться, в чем может быть проблема. В идеале вы должны включить минимальный воспроизводимый пример, демонстрирующий проблемное поведение (включая конкретные измерения времени). Это значительно улучшит вопрос и даст ему право на голосование.

Theodor Zoulias 19.03.2022 12:44

Вы правы @TheodorZoulias, я так и сделаю

klop52 19.03.2022 15:07
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
6
153
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вы можете использовать оператор 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 18.03.2022 12:28

@ klop52 хм, я ожидаю, что это будет довольно быстро. Не могли бы вы измерить время с помощью Stopwatch, запустить его непосредственно перед ToLookup и остановить сразу после внешнего цикла foreach и сообщить ElapsedMilliseconds? Я подозреваю, что есть что-то еще, что медленно, до или после, и влияет на общую скорость.

Theodor Zoulias 18.03.2022 12:35

Время выполнения для подготовки данных со старой версией: 0ч 0м 10с 358мс ; Время выполнения для подготовки данных с обновленной версией: 0ч 0м 9с 83мс.

klop52 18.03.2022 12:38

@ klop52 сколько строк (приблизительно) в таблицах Table_1 и Table_2?

Theodor Zoulias 18.03.2022 12:40

Я пробую это сейчас в течение 23 дней и около 430 идентификаторов.

klop52 18.03.2022 12:41

Спасибо за помощь, на днях попробую

klop52 19.03.2022 12:03
Ответ принят как подходящий

Я рекомендую использовать другой подход. 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 для этой таблицы и внести другие незначительные изменения...

klop52 18.03.2022 18:47

Я не был уверен, стоит ли мне открывать новую тему, у меня есть короткий вопрос: как правильно зациклиться на Table_1? (Я просто борюсь с этим и не могу найти никакого способа, любая ссылка или подсказка будут оценены)

klop52 18.03.2022 20:15

@ klop52 Зависит от того, почему вы зацикливаетесь? Что вы пытаетесь достичь? Самый простой — это foreach (DataRow dr in Table_1.Rows) или foreach (var dr in Table_1.AsEnumerable()), как добавление цикла в ответе.

NetMage 21.03.2022 18:41

Иногда мне также нужно сравнить значения между строками, поэтому цикл 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 22.03.2022 00:14

@ klop52 Не должно быть столбца «A» - все они «A_#» на основе «ID». Кроме того, всегда используйте Field<T>("column name") вместо [], чтобы убедиться, что ваш == использует правильные типы.

NetMage 22.03.2022 01:26

А, правда, извините. Спасибо за помощь! Я попробую

klop52 22.03.2022 09:55

Другие вопросы по теме