模拟使用 contains 运算符而不是 equals 的连接?

Emulating a join which uses a contains operator opposed to equals?

我发现 join 运算符 does not allow the use of Contains and thus only performs equijoins。但是,我需要执行 "not equijoin".

我特别需要使用以下设置编写查询。给定两种类型的对象 ClassStudent

public class Class
{
    public string Name { get; set; } = "";
    public List<Guid> Students { get; set; } = new List<Guid>();
}

public class Student
{
    public Guid StudentId { get; set; } = Guid.NewGuid();
    public int Grade { get; set; } = 0;
}

其中 Class 通过其 StudentId 引用其 Student。我想写一个子句,找到所有 Class,其中所有 Student 的平均成绩高于某个值。


class Program
{
    static void Main(string[] args)
    {
        // Create all of the students
        var class1Students = new List<Student>()
        {
            new Student() {Grade = 70 },
            new Student() {Grade = 70 }
        };
        var class2Students = new List<Student>()
        {
            new Student() {Grade = 80 },
            new Student() {Grade = 80 }
        };
        var class3Students = new List<Student>()
        {
            new Student() {Grade = 90 },
            new Student() {Grade = 90 }
        };
        var allStudents = new List<Student>();
        allStudents.AddRange(class1Students);
        allStudents.AddRange(class2Students);
        allStudents.AddRange(class3Students);

        // Create all of the classes
        var class1 = new Class()
        {
            Name = "Class1",
            Students = class1Students.Select(s => s.StudentId).ToList()
        };
        var class2 = new Class()
        {
            Name = "Class2",
            Students = class2Students.Select(s => s.StudentId).ToList()
        };
        var class3 = new Class()
        {
            Name = "Class3",
            Students = class3Students.Select(s => s.StudentId).ToList()
        };
        var allClasses = new List<Class>() { class1, class2, class3  };

        // Get all classes where the average grade is above 70
        var query = from cls in allClasses
                    join std in allStudents on 


    }
}

我想这样写查询

var query = from cls in allClasses
    join std in allStudents on cls.Students.Contains(std.StudentId) into clsStds
    where clsStds.Select(aStd => aStd.Grade).Average() > 70
    select cls;

尽管这显然是无效语法。 page linked above 提供了一个非等值连接的示例,尽管我已尝试在此处应用它但似乎无法正确重现它(and/or 我已经严重混淆了自己)。

如何模拟上面描述的 join 类型?

您使用的是 Entity Framework 还是类似的?你有实体之间的导航属性吗?如果是这样,也许您可​​以 GroupBy 学生,然后导航到父对象。类似于:

    var query = allStudents
        .GroupBy(i=>i.Class)
        .Select(i=>new{
            Class = i,
            Average = i.Average(j=>j.Grade)
        })
        .Where(i=>i.Average > 70)
        .ToList();

我找到了一种解决问题的简单方法。首先,我们 "resolve" student 中的 Guid 引用实际 Student 对象

var clsStd = 
    from cls in allClasses
    select new
    {
        cls,
        stdObjs = allStudents.Where(aStd => cls.Students.Contains(aStd.StudentId))
    };

然后我们可以查询这个匿名对象集合

var classes = 
   from clsStdObj in clsStd
   where clsStdObj.stdObjs.Select(stdObj => stdObj.Grade).Average() > 70
   select clsStdObj.cls;

然后解析为 "set of all classes in which the average student grade is above 70"。

但我仍然愿意接受不那么幼稚的解决方案。

这可能是一个解决方案(感谢@EricLippert 关于使用 Any() 以避免 classGrades 为空时可能发生的崩溃的建议):

var query = from _class in allClasses
            let classGrades = from std in allStudents
                           where _class.Students.Contains(std.StudentId)
                           select std.Grade
            where classGrades.Any()
            where classGrades.Average() > 70
            select _class;

本质上,对于每个 Class,我都创建了一个新的子查询,其中我 select Student 中的所有 Class,我只投射Grade,所以结果是 IEnumerable<string>。下一部分很简单:Average() 成绩。

出于好奇,我使用 Stopwatch 将你的解决方案与我的进行比较,即使它纯粹是指示性的,延迟时间(我的/你的)之间的比率约为 0.02。

首先,你的数据模型是错误的。学生没有单一的成绩。 他们的成绩在 class,而您的模型没有考虑到这一点。您需要第三个 table,包含学生、class 和成绩列。我强烈建议您解决这个问题。

按照说明解决问题很简单,但我不喜欢目前提供的任何解决方案。它们大多是合理的,但可以更加高效和稳健。

您遇到的根本问题是:您没有从学生 ID 到学生对象的快捷方式。 先解决那个问题:

var idToStudent = allStudents.ToLookup(s => s.id);

太棒了。现在解决方案很简单:

var query = 
  from cls in allClasses
  let grades = from id in cls.Students select idToStudent(id).Grade
  where grades.Any()
  where grades.Average() > 70
  select cls;

请注意,我们正在测试是否有 Any 成绩,因为可能 class 没有学生。 Average 如果要求取零项的平均值会崩溃,所以检查是明智的。

当您修复数据模型以便正确关联学生、classes 和成绩时,它将是:

var query = 
  from cls in allClasses
  let grades = 
    from grade in allGrades 
    where grade.Class == cls 
    select grade.grade
  where grades.Any()
  where grades.Average() > 70
  select cls;

在一组设计合理的 table 中,计算平均值时不会考虑学生 ID;您直接将成绩和 classes 联系起来。

现在,您开始提出这个问题时指出您需要一种 C# 不支持的联接。不,您需要修复数据关系,然后 C# 支持您需要的连接类型!上面可以更有效地写成

var query = 
  from cls in allClasses
  join g in allGrades on cls equals g.Class into grades
  where grades.Average() > 70
  select cls;

不再需要检查 Any,因为 C# 不会生成空组。

这就是您需要的加入;正确设计您的 table 然后使用它!