以更快的性能将 LINQ 查询结果复制到现有 DataTable 中

Copy LINQ query results into existing DataTable with faster performance

我正在寻找一种提高 C# 代码性能的方法,希望得到任何帮助。

有2个table:Table_1和Table_2,我想从Table_2中采集数据保存在下面的Table_1中形式:

Table_1

Date Some_Stat
2021-06-01 23.7
2021-06-02 12.6
2021-06-03 47.9

Table_2

Date ID A B C
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

我的目标:加入后Table_1

Date Some_Stat A_3 B_3 C_3 A_4 B_4 C_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

为了实现这一点,我使用了下面的代码,该代码有效,但速度很慢,尤其是当有数千个 ID 时。代码所做的基本上是首先进行所需的连接并将 LINQ 连接结果传输到现有 Table_1(复制列)。 我检查了性能时间,LINQ 查询总是非常快(时间:0 毫秒),但数据传输是问题。

List<int> ids = Table_2.AsEnumerable().Select(s => s.Field<int>("ID").Distinct().ToList();

for (int i = 0; i < 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; i < 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_1 中从 Table_2 收集数据的新方法。可能有一种方法可以使用内置函数(用 C 或 C++ 编写)来直接访问机器代码(例如,将列从一个 table 复制到另一个 table 的函数,如python) 以避免遍历行。

注意:在原始问题中,Table_1 可能有多达 500 行和 10 列,而 Table_2 每天可能有多达 5000 行,填充率为 50 - 100%。上述解决方案在极端情况下需要 2 到 4 个小时,这非常慢(我相信这可以在几分钟内完成)。

您可以使用 ToLookup LINQ 运算符,并根据 Table_2 的内容创建高效的 Lookup<DateTime, DataRow> 结构。此只读结构将为每个唯一日期包含一个 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。显然 DataTables 没有针对这样的场景进行优化。更新 DataRow 的任何列的复杂度是 O(n²),其中 N 是列的总数。要克服这个问题,您可以通过 ItemArray property, manipulate these values, and finally import them back through the same property. The performance can be improved even further by using the BeginLoadData and EndLoadData 方法导出存储在 DataRow 中的所有值。

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

我的建议是使用不同的方法。 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;

Table_1 中测试 100 天,在 Table_2 中每天 500 行,75% 的人口,我得到大约 128 倍的加速。