【问题标题】:How to make left join, group by and select with the single query?如何使用单个查询进行左连接、分组和选择?
【发布时间】:2017-08-20 07:35:49
【问题描述】:

假设我有以下数据:

var workers = new[]
{
    new {  Name = "John",  Id = 1 },
    new {  Name = "Greg",  Id = 2 }, 
    new {  Name = "Jack",  Id = 3 }, 
    new {  Name = "Josh",  Id = 4 },
    new {  Name = "Jill",  Id = 5 },
    new {  Name = "Jane",  Id = 6 }
};

var contracts = new[]
{
    new {  ContractNumber="1",  WorkerId=1, ContractDate = new DateTime(2017,6,30) },
    new {  ContractNumber="2",  WorkerId=2, ContractDate = new DateTime(2017,7,10) },
    new {  ContractNumber="3",  WorkerId=2, ContractDate = new DateTime(2017,7,15) },
    new {  ContractNumber="4",  WorkerId=5, ContractDate = new DateTime(2017,7,20) },
    new {  ContractNumber="5",  WorkerId=1, ContractDate = new DateTime(2017,7,25) }
};

我需要做的是选择合同日期大于或等于的合同数量最少的第一个工人:

var fromDate = new DateTime(2017, 7, 1);

排除具有以下 ID 的工人:

int[] exceptWorkerIds = new int[] {1, 4};

如果多个工人的合同最小数量相似,则按字母顺序选择名字中的工人。

我通过以下方式解决了这个任务。

首先,每个工人离开加入合同。如果合同存在,我的辅助属性 ContractExists = 1,如果不存在则为 0。

var query = 
from w in workers.Where(x => !exceptWorkerIds.Contains(x.Id))
join c in contracts.Where(x => x.ContractDate >= fromDate) 
    on w.Id equals c.WorkerId into workerContracts
from wc in workerContracts.DefaultIfEmpty()
select new {WorkerId = w.Id, WorkerName = w.Name, ContractExists = wc == null ? 0: 1};

这个查询给了我以下结果:

其次,我将得到的结果按WorkerId分组,WorkerName通过sum和worker name得到合同和订单数据的总和:

var result = 
(from q in query
group q.ContractExists by new {q.WorkerId, q.WorkerName} into g
orderby g.Sum(), g.Key.WorkerName
select new 
{
    WorkerId = g.Key.WorkerId, 
    WorkerName = g.Key.WorkerName, 
    WorkerContractsCount = g.Sum()
}).ToList().Take(1);

Take(1) 给了我结果数据的前 1 个:

问题:有没有办法用唯一的查询或任何比我更简单或优雅的方式来做到这一点?如果是,这是否有助于提高查询执行的效率?

【问题讨论】:

  • workers.FirstOrDefault(w => !contracts.Any(c => c.WorkerId == w.Id)) 会给你第一个没有任何合同的工人。如果为 null,则可以使用内部连接找到您搜索的工作人员。不是答案,因为它会导致 2 个查询,而不仅仅是 1 个,但仍然 :)
  • 有趣。如果不为null,则不需要执行第二个查询。
  • 是的,在您的示例中,它将更快地为您提供 Jack。

标签: c# linq


【解决方案1】:

你可以使用group join(在你做from wc in workerContracts.DefaultIfEmpty()之前你的查询实际上使用的是什么)而不是join(将数据相乘)然后是group by )。

其他逻辑几乎相同 - workerContracts.Count() 为您提供所需数量的合约,因此应用所需的订单,取第一个,您就完成了:

var result =
    (from w in workers.Where(x => !exceptWorkerIds.Contains(x.Id))
     join c in contracts.Where(x => x.ContractDate >= fromDate)
         on w.Id equals c.WorkerId into workerContracts
     let workerContractsCount = workerContracts.Count()
     orderby workerContractsCount, w.Name
     select new
     {
         WorkerId = w.Id,
         WorkerName = w.Name,
         WorkerContractsCount = workerContractsCount
     })
    .FirstOrDefault();

【讨论】:

    【解决方案2】:

    Wheres 和 Join 可能比 Ivan Stoev's answer 少,这是一个更紧凑的版本:

    var result = workers
        .Where(w => !exceptWorkerIds.Contains(w.Id))
        .Select(w => new { 
            Name = w.Name, 
            Id = w.Id, 
            Nb = contracts
                .Count(c => c.WorkerId == w.Id && c.ContractDate >= new DateTime(2017,7,1))
        })
        .OrderBy(w => w.Nb).ThenBy(w => w.Name).FirstOrDefault();
    
    if(result != null)
        Console.WriteLine(result.Name);
    else
        Console.WriteLine("Result not found");
    

    解释:对于每个工人,除了我们不想检查的工人,我们计算日期晚于或等于2017,7,1的合同数量,然后按此数字和名称对其进行排序,并取第一个。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-07-08
      • 1970-01-01
      • 2021-06-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-02-10
      相关资源
      最近更新 更多