以更快的性能将 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
。显然 DataTable
s 没有针对这样的场景进行优化。更新 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 倍的加速。
我正在寻找一种提高 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
。显然 DataTable
s 没有针对这样的场景进行优化。更新 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 倍的加速。