LINQ加入以返回动态列列表

时间:2019-01-24 21:49:48

标签: c# linq datatable

我正在寻找一种从两个数据表的LINQ连接返回动态列列表的方法。

首先,这不是重复项。我已经研究并丢弃了:

C# LINQ list select columns dynamically from a joined dataset

Creating a LINQ select from multiple tables

How to do a LINQ join that behaves exactly like a physical database inner join?

(还有许多其他)

这是我的出发点:

public static DataTable JoinDataTables(DataTable dt1, DataTable dt2, string table1KeyField, string table2KeyField, string[] columns) {
   DataTable result = ( from dataRows1 in dt1.AsEnumerable()
                        join dataRows2 in dt2.AsEnumerable()
                        on dataRows1.Field<string>(table1KeyField) equals dataRows2.Field<string>(table2KeyField)
                        [...I NEED HELP HERE with the SELECT....]).CopyToDataTable();
   return result;
}

一些注意事项和要求:

  1. 没有数据库引擎。数据源是大型的CSV文件(超过50万条记录),它们被读取到c#DataTable s中。
  2. 由于CSV很大,因此出于性能原因,遍历联接中的每个记录是一个不好的解决方案。我已经尝试过记录循环,但是它太慢了。我在上面的联接上获得了不错的性能,但是我找不到一种方法可以让它仅返回我想要的列(由调用方指定)而不循环记录。
  3. 如果我需要在联接中遍历,那很好,我只是不想循环行。
  4. 我希望能够传递一个列名数组,并仅在结果DataTable中返回那些列。如果传入的两个数据表都碰巧有一个相同的列,并且该列在我的列名称数组中,则只需传回任一列,因为在这种情况下,两列之间的数据相同。
  5. 如果我需要传递2个数组(每个数据表的所需列为1个),那很好,但是理想的是1个列名数组。
  6. 列列表不能是静态的,也不能硬编码到函数中。原因是因为我的JoinDataTables()是从系统中的许多不同位置调用的,以便连接各种CSV转换数据表,并且每个CSV文件都有非常不同的列。
  7. 我不希望在结果DataTable中返回 all 列,而只是在columns数组中指定的列。

因此,假设在调用JoinDataTables()之前,我具有以下2个数据表:

Table: T1
T1A T1B T1C T1D
==================
10  AA  H1  Foo1
11  AB  H1  Foo2
12  AA  H2  Foo1
13  AB  H2  Foo2

Table: T2
T2A T2X T2Y T2Z
==================
12  N1  O1  Yeah1
17  N2  O2  Yeah2
18  N3  O1  Yeah1
19  N4  O2  Yeah2

现在假设我们像这样连接这两个表: ON T1.T1A = T2.T2A

select * from [join]

并产生以下结果集:

T1A T1B T1C T1D   T2A T2X T2Y T2Z
====================================
12  AA  H2  Foo1  12  N1  O1  Yeah1

请注意,联接仅产生1行。

现在到我的问题的症结所在。假设对于给定的用例,我只想从此联接中返回4列:T1A,T1D,T2A和T2Y。因此,我的结果集将如下所示:

T1A T1D   T2A  T2Y
==================
12  Foo1  12   O1

我希望能够这样调用我的JoinDataTables函数:

DataTable dt = JoinDataTables(dt1, dt2, "T1A", "T2A", new string[] {"T1A", "T1D", "T2A", "T2Y"});

考虑到性能以及我不想遍历记录这一事实(因为处理大量数据的速度很慢),该如何实现? (该连接已经运行良好,现在我只需要一个正确的select段(无论是通过new{..}还是您想的方式)。

我无法接受函数内部具有硬编码列列表的解决方案。我在SO上都找到了这种方法的示例。

有什么想法吗?

编辑:我可以每次都取回所有列,但我尝试包括所有列的每一次尝试都导致某种形式的FULL OUTER JOIN或CROSS JOIN,返回了数量级更高的记录比应该的因此,只要不交叉连接,我就会开放所有列。

1 个答案:

答案 0 :(得分:0)

我不确定50万条记录的性能,但这是一个尝试的解决方案。

由于您要合并来自不同表的DataRow的两个子集,因此没有简单的操作可以创建子集或从子集创建新的DataTable(尽管我有一个扩展方法将IEnumerable<anon>中的anon = new { DataRow1, DataRow2, ... }从联接中展平,这对您来说可能很慢。

相反,我使用请求的列预先创建了一个答案DataTable,然后使用LINQ来构建要添加为行的值数组。

public static DataTable JoinDataTables(DataTable dt1, DataTable dt2, string table1KeyField, string table2KeyField, string[] columns) {
    var rtnCols1 = dt1.Columns.Cast<DataColumn>().Where(dc => columns.Contains(dc.ColumnName)).ToList();
    var rc1 = rtnCols1.Select(dc => dc.ColumnName).ToList();
    var rtnCols2 = dt2.Columns.Cast<DataColumn>().Where(dc => columns.Contains(dc.ColumnName) && !rc1.Contains(dc.ColumnName)).ToList();
    var rc2 = rtnCols2.Select(dc => dc.ColumnName).ToList();

    var work = from dataRows1 in dt1.AsEnumerable()
               join dataRows2 in dt2.AsEnumerable()
               on dataRows1.Field<string>(table1KeyField) equals dataRows2.Field<string>(table2KeyField)
               select (from c1 in rc1 select dataRows1[c1]).Concat(from c2 in rc2 select dataRows2[c2]).ToArray();

    var result = new DataTable();
    foreach (var rc in rtnCols1)
        result.Columns.Add(rc.ColumnName, rc.DataType);
    foreach (var rc in rtnCols2)
        result.Columns.Add(rc.ColumnName, rc.DataType);

    foreach (var rowVals in work)
        result.Rows.Add(rowVals);

    return result;
}

由于您使用的是查询语法,所以我也这样做了,但是通常我可能会像这样:select

select rc1.Select(c1 => dataRows1[c1]).Concat(rc2.Select(c2 => dataRows2[c2])).ToArray();

已更新:通过替换DataRowrc1的定义,可能值得使用列序号而不是名称来索引每个rc2

var rc1 = rtnCols1.Select(dc => dc.Ordinal).ToList();
var rc1Names = rtnCols1.Select(dc => dc.ColumnName).ToHashSet();
var rtnCols2 = dt2.Columns.Cast<DataColumn>().Where(dc => columns.Contains(dc.ColumnName) && !rc1Names.Contains(dc.ColumnName)).ToList();
var rc2 = rtnCols2.Select(dc => dc.Ordinal).ToList();