模拟使用 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".
我特别需要使用以下设置编写查询。给定两种类型的对象 Class
和 Student
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 然后使用它!
我发现 join
运算符 does not allow the use of Contains
and thus only performs equijoins。但是,我需要执行 "not equijoin".
我特别需要使用以下设置编写查询。给定两种类型的对象 Class
和 Student
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 然后使用它!