【问题标题】:Left Join/merge datatable c# with non-unique columns左联接/合并具有非唯一列的数据表 c#
【发布时间】:2019-06-05 02:22:50
【问题描述】:

我有两个具有不同结构的数据表,A 表有一个名为“Campaign ID”的列,它不是唯一的,我想将它加入到 B 表的唯一“Campaign ID”中。

所以它相当于 sql 中的内容

select * from 
A left join B on 
A.[Campagin ID] = B.[Cmpaign ID]

我试过 datatable.merge 不起作用,因为它只能基于唯一的列字段进行合并。

我已经尝试过 Linq 和 Lamda。

var resultDt = from c in dt.AsEnumerable()
                       join lookup in lookupDt.AsEnumerable() on c["Campaign ID"].ToString() equals lookup["EventID"]
                           .ToString() into results
                       from r in results.DefaultIfEmpty()
                       select new { a=c, b =lookup };

它返回两组数据行而不是一组数据行。

我也尝试过使用字典,但运行起来太贵了。

预期结果 如果我选择 r,它将只返回表 B 值

I expected the output would be like
select * from 
A left join B on 
A.[Campagin ID] = B.[Cmpaign ID]

在 SQL 中

如果表 A 是这样的

Campaign ID                            Description      Number
eda1e64c-0002-4000-8000-000000000198            
eda1e64c-0002-4000-8000-000000000198            
eda1e64c-0002-4000-8000-000000000198            
eda1e64c-0002-4000-8000-000000000198            
eda1e64c-0002-4000-8000-000000000000    Testing 123     1111
                                        Description 2   3333

表 B 是这样的

Campaign ID                             Name      
eda1e64c-0002-4000-8000-000000000198    Test Name1  
eda1e64c-0002-4000-8000-000000000000    Test Name2      

预期结果

Campaign ID                             Description      Number   Name
eda1e64c-0002-4000-8000-000000000198                             Test Name1
eda1e64c-0002-4000-8000-000000000198                             Test Name1
eda1e64c-0002-4000-8000-000000000198                             Test Name1
eda1e64c-0002-4000-8000-000000000198                             Test Name1
eda1e64c-0002-4000-8000-000000000000    Testing 123     1111     Test Name2

是否有任何我可以使用的默认 c# 方法,或者任何有效的方法来做到这一点? 非常感谢所有的帮助。

【问题讨论】:

  • 请更新以显示示例数据,以便我们可视化查询正在执行的操作。
  • 您目前在输出中得到了什么。因为你的 join mysql 查询对我来说很好
  • 您能否将示例数据显示为填充DataTables 的有效C# 代码?
  • “它返回两组数据行”——公平地说,当你说想要一个具有两个属性的新对象时,你告诉它要做的事情,他们两个数据行?您是否期望一个数据行?
  • 是的,我期待一行,我想通了,但是因为有 30 多个字段,如果我不必逐个输入,那就太好了,我看到人们添加了两个数据行不幸的是,它对我不起作用。

标签: c#


【解决方案1】:

我想你快到了,只需将你的 LINQ 查询输出转换为一个对象数组,然后将它作为一个单独的东西放入一个新的数据表中;请记住,LINQ 主要用于查询和返回结果集合,而不是修改现有内容:

使用LINQ左连接,手动输出列表,手动消费到数据表中

            var query =
                from ce in c.AsEnumerable()
                join le in lookup.AsEnumerable() on c.Field<Guid>("Campaign ID") equals le.Field<Guid>("Campaign ID") into cele
                from lenull in cele.DefaultIfEmpty()
                select new object[]
                {
                  ce.Field<Guid>("Campaign ID"),
                  ce.Field<string>("Description"),
                  ce.Field<int>("Number"), //don't know how your table has null here, maybe <int?>
                  lenull?.Field<string>("Name")
                };

            DataTable c = new DataTable(); //to hold results
            c.Columns.Add("Campaign ID", typeof(Guid)); 
            c.Columns.Add("Description"); 
            c.Columns.Add("Number", typeof(int)); 
            c.Columns.Add("Name");
            foreach (var at in query)
                c.Rows.Add(at);

因为 lenull 可能为 null,所以我使用 null 传播器来避免尝试获取 null 行的字段的 null 引用异常。我们也可以动态地执行此操作,无需反射,但速度要慢得多。对于以下示例,我使用了自己的一对简单数据表,设置如下:

        //setup part
        DataTable a = new DataTable();
        a.Columns.Add("ID", typeof(int));
        a.Columns.Add("Name", typeof(string));
        a.Columns.Add("Age", typeof(int));
        DataTable b = new DataTable();
        var pk = b.Columns.Add("ID", typeof(int));
        b.Columns.Add("Address", typeof(string));
        b.Columns.Add("YearsAt", typeof(int));
        b.PrimaryKey = new[] { pk };

        a.Rows.Add(1, "John", 22);
        a.Rows.Add(2, "Mary", 33);
        a.Rows.Add(3, "Bill", 44);

        b.Rows.Add(1, "JohnAddr", 3);
        b.Rows.Add(2, "MaryAddr", 4);

Left Join with LINQ,手动输出列表,动态消费

            var query =
                from ae in a.AsEnumerable()
                join be in b.AsEnumerable() on ae.Field<int>("ID") equals be.Field<int>("ID_") into aebe
                from be2 in aebe.DefaultIfEmpty()
                select new Dictionary<string, object>
                {
                    {"ID", ae.Field<int>("ID")},
                    {"Name", ae.Field<string>("Name") },
                    {"Age", ae.Field<int>("Age") },
                    {"Address", be2?.Field<string>("Address") },
                    {"YearsAt", be2?.Field<int>("YearsAt") }
                };

            //setup datatable
            DataTable c = new DataTable();                    

            int keyCount = query.First().Keys.Count; //track columns needed to be added
            foreach (var dict in query)
            {
                var ro = c.NewRow();
                foreach (string key in dict.Keys)
                {
                    if (keyCount > 0 && dict[key] != null && !c.Columns.Contains(key))
                    { //if the column is not in the table, and the value isnt null (so we can deduce the type)
                        c.Columns.Add(key, dict[key].GetType());
                        keyCount--; //mark it as added. Eventually this will hit 0 and we won't evaluate the other two clauses
                    }

                    if (dict[key] != null) //don't store nulls
                        ro[key] = dict[key];
                }
                c.Rows.Add(ro);
            } 

当然,您可能会抱怨您仍然必须在 LINQ 查询选择中指定您想要的所有列。我们也可以让它变得动态:

Left Join with LINQ,动态输出列表,动态消费

             var query =
                from ae in a.AsEnumerable()
                join be in b.AsEnumerable() on ae.Field<int>("ID") equals be.Field<int>("ID_") into aebe
                from be2 in aebe.DefaultIfEmpty()
                select MapToDict(ae, be2);

            //setup datatable
            DataTable c = new DataTable();                    

            int keyCount = query.First().Keys.Count;
            foreach (var dict in query)
            {
                //have we got all our columns addded yet?
                var ro = c.NewRow();
                foreach (string key in dict.Keys)
                {
                    if (keyCount > 0 && dict[key] != null && !c.Columns.Contains(key))
                    { //if the column is not in the table, and the value isnt null (so we can deduce the type)
                        c.Columns.Add(key, dict[key].GetType());
                        keyCount--; //mark it as added. Eventually this will hit 0 and we won't evaluate the other two clauses
                    }

                    if (dict[key] != null) //don't store nulls
                        ro[key] = dict[key];
                }
                c.Rows.Add(ro);
            }

我从不喜欢 LINQ 中的 DataTables 连接,我一直更喜欢:

  • 在b上建立主键
  • 向与 b 的列名和类型重复的 a 添加新列(如果存在名称冲突,重命名 b 列添加一个 int)
  • 遍历 a,调用 b.Find(a 中的某些列)
  • 如果 find 没有返回 null,对于 b 中的每一列,将 a 行中的同名列设置为 find 给你的 b 行中的值

这是执行上述操作的代码:

使用循环左连接

        //ensure unique named columns in b, and grow a's columns
        foreach (DataColumn bcol in b.Columns) {
            while (a.Columns.Contains(bcol.ColumnName))
                bcol.ColumnName += "_";
            a.Columns.Add(bcol.ColumnName, bcol.DataType);
        }

        //perform left join
        foreach (DataRow aro in a.Rows) {
            var f = b.Rows.Find(aro["ID"]);
            if (f != null)
                foreach (DataColumn bcol in b.Columns)
                    aro[bcol.ColumnName] = f[bcol];
        }

将其转换为扩展方法可能相当简单,这样任何表都可以像 a.LeftJoin(b, aID: "ID", bID: "ID") 那样加入另一个表。如果你想要一个比简单等于更复杂的逻辑,那么就需要进行一些代码更改。

出于好奇,我背靠背尝试了所有 4 种方法,并为它们计时。在我的上下文中,循环比具有固定结构和硬编码列名的 LINQ 快约 2.5 倍,比使用字典使事物动态化快 4 倍:

        for (int lc = 0; lc < 10; lc++) {

            //setup 100K rows
            DataTable a = new DataTable();
            a.Columns.Add("ID", typeof(int));
            a.Columns.Add("Name", typeof(string));
            a.Columns.Add("Age", typeof(int));
            DataTable b = new DataTable();
            var pk = b.Columns.Add("ID", typeof(int));
            b.Columns.Add("Address", typeof(string));
            b.Columns.Add("YearsAt", typeof(int));
            b.PrimaryKey = new[] { pk };

            Random r = new Random();
            for (int i = 0; i < 100000; i++)
            {
                a.Rows.Add(i, Guid.NewGuid().ToString(), r.Next(20, 99));
                if (r.Next(0, 9) < 1)
                    b.Rows.Add(i, Guid.NewGuid().ToString(), r.Next(1, 10));

            }

            Stopwatch sw = Stopwatch.StartNew();

### INSERT CHOSEN METHOD HERE ###

            sw.Stop();

            Console.WriteLine($"Time: {sw.ElapsedMilliseconds}ms");

        }

循环处理 100K 行的结果通常为 80 毫秒,LINQ 硬代码(手动选择、手动表)为 200 毫秒,LINQ 字典(动态内容)方法为 400 毫秒。

【讨论】:

  • 这是我们长期以来一直使用它的方式,但是我们总是最终编写/调用相当多的冗余代码,如果数据表很大,性能就不那么好了。非常感谢您的帮助。
  • 我之前的代码有些问题;我已经修改了上面的答案以提供更多细节
  • 这真的很详细,非常感谢您的回复:)
【解决方案2】:

比如:

TableAlist.Select(A => A.CampaignId, A.Description, A.Number, 
     Name = TableBlist.FirstOrDefault(B => B.CampaignId == A.CampaignId)?.Name ?? "").ToList()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-11-09
    • 2015-06-21
    • 2014-10-03
    • 2016-05-01
    • 1970-01-01
    • 2012-01-15
    • 2013-01-05
    相关资源
    最近更新 更多