目 录CONTENT

文章目录

CSharp(五十三) LINQ 联接与分组详解

C# LINQ 联接与分组详解


一、联接和分组做什么?

1.1 生活比喻

联接(Join) 就像是:

你有两份名单——学生名单上只有城市名,城市评级表上有城市的 GDP 和等级。你想把两份表"对"起来,给每个学生补上他所在城市的信息。这就是联接。

分组(GroupBy) 就像是:

一堆散落的学生档案,你要按城市把它们分成几摞——北京的放一摞,上海的放一摞。每摞上贴个标签,这就是分组。

1.2 一句话理解

联接 = Join = 把两张表按某个关联键拼在一起
分组 = GroupBy = 把一张表按某个标准拆成一组一组

二、准备测试数据

以下所有示例都基于这两份数据:

using System;
using System.Collections.Generic;
using System.Linq;

// ===== 学生表 =====
public class Student
{
    public int Id;
    public string Name;
    public int Age;
    public int Score;
    public string City;      // 关联键
    public string Subject;
}

List<Student> students = new List<Student>
{
    new Student { Id = 1, Name = "张三", Age = 18, Score = 92, City = "北京", Subject = "数学" },
    new Student { Id = 2, Name = "李四", Age = 19, Score = 85, City = "上海", Subject = "数学" },
    new Student { Id = 3, Name = "王五", Age = 18, Score = 76, City = "北京", Subject = "语文" },
    new Student { Id = 4, Name = "赵六", Age = 20, Score = 58, City = "广州", Subject = "数学" },
    new Student { Id = 5, Name = "孙七", Age = 19, Score = 88, City = "上海", Subject = "语文" },
    new Student { Id = 6, Name = "周八", Age = 20, Score = 95, City = "北京", Subject = "英语" },
    new Student { Id = 7, Name = "吴九", Age = 18, Score = 45, City = "广州", Subject = "英语" },
    new Student { Id = 8, Name = "郑十", Age = 19, Score = 72, City = "深圳", Subject = "数学" },
};

// ===== 城市信息表 =====
public class CityInfo
{
    public string City;       // 关联键
    public string Province;
    public string Rating;
    public int GDP;           // 亿元
}

List<CityInfo> cities = new List<CityInfo>
{
    new CityInfo { City = "北京", Province = "北京",   Rating = "一线", GDP = 40000 },
    new CityInfo { City = "上海", Province = "上海",   Rating = "一线", GDP = 43000 },
    new CityInfo { City = "广州", Province = "广东",   Rating = "一线", GDP = 28000 },
    new CityInfo { City = "深圳", Province = "广东",   Rating = "一线", GDP = 30000 },
    new CityInfo { City = "杭州", Province = "浙江",   Rating = "新一线", GDP = 18000 },
    new CityInfo { City = "成都", Province = "四川",   Rating = "新一线", GDP = 20000 },
};

第一部分:联接(Join)


三、Join —— 内连接(INNER JOIN)

3.1 什么是内连接?

只返回两张表中能匹配上的数据。 匹配不上的(如杭州、成都没有学生)不出现。

// 学生表和城市表做联接,关联键是 City
var joined = students.Join(
    cities,                 // 1. 第二张表
    s => s.City,            // 2. 学生表用什么字段关联
    c => c.City,            // 3. 城市表用什么字段关联
    (s, c) => new           // 4. 匹配成功后生成什么结果
    {
        s.Name,
        s.City,
        c.Province,
        c.Rating,
        s.Score
    }
);

foreach (var item in joined)
    Console.WriteLine($"{item.Name,-6} | {item.City,-4} | {item.Province,-4} | {item.Rating} | {item.Score}分");

输出:

张三    | 北京  | 北京  | 一线 | 92分
李四    | 上海  | 上海  | 一线 | 85分
王五    | 北京  | 北京  | 一线 | 76分
赵六    | 广州  | 广东  | 一线 | 58分
孙七    | 上海  | 上海  | 一线 | 88分
周八    | 北京  | 北京  | 一线 | 95分
吴九    | 广州  | 广东  | 一线 | 45分
郑十    | 深圳  | 广东  | 一线 | 72分

注意:杭州和成都在城市表中有,但没有学生,所以不出现。这就是内连接。

3.2 查询语法写 Join(更直观)

// 查询语法——像写 SQL 一样
var joined = from s in students
             join c in cities on s.City equals c.City
             select new
             {
                 s.Name,
                 s.City,
                 c.Province,
                 c.Rating,
                 s.Score
             };

// 和上面方法语法的结果一模一样

查询语法中 join 的格式:

join 范围变量 in 第二张表 on 左键 equals 右键

注意:是 equals 不是 ==!这是 LINQ 的固定语法。

3.3 Join 后加筛选和排序

// 联表 + 筛选 + 排序
var result = from s in students
             join c in cities on s.City equals c.City
             where s.Score >= 60 && c.Rating == "一线"     // 联表后筛选
             orderby s.Score descending                     // 联表后排序
             select new
             {
                 s.Name,
                 s.City,
                 c.Province,
                 c.Rating,
                 s.Score
             };

foreach (var item in result)
    Console.WriteLine($"{item.Name,-6} | {item.City,-4} | {item.Province,-4} | {item.Rating} | {item.Score}分");

输出:

周八    | 北京  | 北京  | 一线 | 95分
张三    | 北京  | 北京  | 一线 | 92分
孙七    | 上海  | 上海  | 一线 | 88分
李四    | 上海  | 上海  | 一线 | 85分
王五    | 北京  | 北京  | 一线 | 76分
郑十    | 深圳  | 广东  | 一线 | 72分

3.4 多表联接(三张表)

// 第三张表:科目信息
public class SubjectInfo
{
    public string Subject;
    public string TeacherName;
    public int Credit;
}

List<SubjectInfo> subjects = new List<SubjectInfo>
{
    new SubjectInfo { Subject = "数学", TeacherName = "陈老师", Credit = 5 },
    new SubjectInfo { Subject = "语文", TeacherName = "刘老师", Credit = 4 },
    new SubjectInfo { Subject = "英语", TeacherName = "王老师", Credit = 4 },
};

// 三表联接
var threeJoined = from s in students
                  join c in cities on s.City equals c.City
                  join subj in subjects on s.Subject equals subj.Subject
                  where s.Score >= 60
                  orderby s.Score descending
                  select new
                  {
                      s.Name,
                      s.City,
                      c.Province,
                      s.Subject,
                      subj.TeacherName,
                      subj.Credit,
                      s.Score
                  };

foreach (var item in threeJoined)
{
    Console.WriteLine($"{item.Name,-6} | {item.City,-4} | {item.Subject,-4} | "
        + $"{item.TeacherName,-6} | {item.Credit}学分 | {item.Score}分");
}

输出:

周八    | 北京  | 英语  | 王老师   | 4学分 | 95分
张三    | 北京  | 数学  | 陈老师   | 5学分 | 92分
孙七    | 上海  | 语文  | 刘老师   | 4学分 | 88分
李四    | 上海  | 数学  | 陈老师   | 5学分 | 85分
王五    | 北京  | 语文  | 刘老师   | 4学分 | 76分
郑十    | 深圳  | 数学  | 陈老师   | 5学分 | 72分

四、GroupJoin —— 分组联接(LEFT JOIN 效果)

4.1 什么是分组联接?

左边表的每条记录都保留,右表中匹配的记录被封装成一个"组"。如果右表没匹配到,这个组就是空的。

相当于 SQL 的 LEFT JOIN

生活比喻:

城市表是"主人列表"(5 个城市),学生表是"访客列表"。现在以城市表为主,看每个城市都有哪些学生。没有学生的城市(杭州、成都)也会出现,只是学生数为 0。

4.2 方法语法

var groupJoined = cities.GroupJoin(
    students,                    // 第二张表
    c => c.City,                 // 城市表的关联键
    s => s.City,                 // 学生表的关联键
    (city, studentGroup) => new  // city: 城市表的单条记录
                                 // studentGroup: 这个城市下的所有学生
    {
        city.City,
        city.Province,
        city.Rating,
        city.GDP,
        StudentCount = studentGroup.Count(),
        AvgScore = studentGroup.Any()
            ? studentGroup.Average(s => s.Score)
            : (double?)null,
        Students = studentGroup   // 可以继续操作组内数据
    }
);

foreach (var item in groupJoined)
{
    Console.Write($"{item.City,-4} ({item.Province}) | {item.Rating} | GDP: {item.GDP}亿 | ");
    if (item.StudentCount > 0)
        Console.WriteLine($"{item.StudentCount}人, 均分{item.AvgScore:F1}");
    else
        Console.WriteLine("无学生");
}

输出:

北京  (北京) | 一线 | GDP: 40000亿 | 3人, 均分87.7
上海  (上海) | 一线 | GDP: 43000亿 | 2人, 均分86.5
广州  (广东) | 一线 | GDP: 28000亿 | 2人, 均分51.5
深圳  (广东) | 一线 | GDP: 30000亿 | 1人, 均分72.0
杭州  (浙江) | 新一线 | GDP: 18000亿 | 无学生
成都  (四川) | 新一线 | GDP: 20000亿 | 无学生

注意:杭州和成都虽然没有学生,但依然出现在结果中!这就是 GroupJoin 和内连接的区别。

4.3 查询语法(用 join ... into)

// 查询语法:join ... into 就是 GroupJoin
var groupJoined = from c in cities
                  join s in students on c.City equals s.City into studentGroup
                  select new
                  {
                      c.City,
                      c.Province,
                      c.Rating,
                      StudentCount = studentGroup.Count(),
                      AvgScore = studentGroup.Any()
                          ? studentGroup.Average(s => s.Score)
                          : (double?)null
                  };

foreach (var item in groupJoined)
{
    Console.Write($"{item.City,-4} ({item.Province}) | {item.Rating} | ");
    if (item.StudentCount > 0)
        Console.WriteLine($"{item.StudentCount}人, 均分{item.AvgScore:F1}");
    else
        Console.WriteLine("无学生");
}

join ... into 语法格式:

from 左表范围变量 in 左表
join 右表范围变量 in 右表 on 左键 equals 右键 into 分组变量
select ...

4.4 GroupJoin 后展开组内数据

// 列出每个城市的具体学生
var detail = from c in cities
             join s in students on c.City equals s.City into studentGroup
             select new
             {
                 c.City,
                 c.Province,
                 Students = studentGroup.Select(s => new
                 {
                     s.Name,
                     s.Subject,
                     s.Score
                 })
             };

foreach (var city in detail)
{
    Console.WriteLine($"--- {city.City} ({city.Province}) ---");
    if (city.Students.Any())
    {
        foreach (var s in city.Students)
            Console.WriteLine($"  {s.Name} - {s.Subject} - {s.Score}分");
    }
    else
    {
        Console.WriteLine("  (无学生)");
    }
}

输出:

--- 北京 (北京) ---
  张三 - 数学 - 92分
  王五 - 语文 - 76分
  周八 - 英语 - 95分
--- 上海 (上海) ---
  李四 - 数学 - 85分
  孙七 - 语文 - 88分
--- 广州 (广东) ---
  赵六 - 数学 - 58分
  吴九 - 英语 - 45分
--- 深圳 (广东) ---
  郑十 - 数学 - 72分
--- 杭州 (浙江) ---
  (无学生)
--- 成都 (四川) ---
  (无学生)

五、Join vs GroupJoin —— 核心对比

特性 Join(内连接) GroupJoin(分组联接)
匹配方式 只保留两边都匹配的 左表全部保留
没匹配的 直接丢弃 以空组形式保留
对应 SQL INNER JOIN LEFT JOIN
结果形状 扁平的一维表 左表行 + 嵌套组
查询语法 join ... on ... equals join ... on ... equals ... into
适用场景 只要匹配数据 需要显示"零记录"

图解区别:

Join(内连接):
  学生表                      城市表
  张三 北京 ──┐            ┌── 北京 一线
  李四 上海 ──┼──匹配──┼── 上海 一线
  王五 北京 ──┘            ├── 广州 一线
                               ├── 深圳 一线
                               ├── 杭州 新一线  ← 没匹配上,丢弃
                               └── 成都 新一线  ← 没匹配上,丢弃

  结果:8 条(学生数),杭州、成都不出现


GroupJoin(分组联接):
  城市表(主表)                学生表
  北京 ───→ [张三, 王五, 周八]
  上海 ───→ [李四, 孙七]
  广州 ───→ [赵六, 吴九]
  深圳 ───→ [郑十]
  杭州 ───→ []  ← 空组,但保留!
  成都 ───→ []  ← 空组,但保留!

  结果:6 条(城市数),全部城市都出现

第二部分:分组(GroupBy)


六、GroupBy —— 基本分组

6.1 什么是 GroupBy?

把集合中的元素按某个键分成一组一组。 每个组是一个 IGrouping<TKey, TElement>,它本身也是一个集合。

// 按城市分组
var byCity = students.GroupBy(s => s.City);

foreach (var group in byCity)
{
    Console.WriteLine($"--- {group.Key} ({group.Count()}人) ---");
    foreach (var s in group)
        Console.WriteLine($"  {s.Name}: {s.Score}分");
}

输出:

--- 北京 (3人) ---
  张三: 92分
  王五: 76分
  周八: 95分
--- 上海 (2人) ---
  李四: 85分
  孙七: 88分
--- 广州 (2人) ---
  赵六: 58分
  吴九: 45分
--- 深圳 (1人) ---
  郑十: 72分

6.2 理解 IGrouping

IGrouping<TKey, TElement> 的核心:

var byCity = students.GroupBy(s => s.City);

foreach (var group in byCity)
{
    string city = group.Key;                         // ← 分组键
    int count = group.Count();                       // ← 组内数量
    double avg = group.Average(s => s.Score);        // ← 组内聚合(可以继续 LINQ)
    int max = group.Max(s => s.Score);               // ← 组内最大值
    var names = group.Select(s => s.Name);           // ← 组内投影

    Console.WriteLine($"{city}: {count}人, 均分{avg:F1}, 最高{max}");
}

输出:

北京: 3人, 均分87.7, 最高95
上海: 2人, 均分86.5, 最高88
广州: 2人, 均分51.5, 最高58
深圳: 1人, 均分72.0, 最高72

七、GroupBy 的高级用法

7.1 分组 + 投影一步到位(方法语法)

// GroupBy 有三个参数:
// 1. keySelector(按什么分组)
// 2. elementSelector(每个元素取什么字段放进组)
// 3. resultSelector(每个组生成什么结果)

var stats = students.GroupBy(
    s => s.City,                      // 1. 按城市分组
    s => s.Score,                     // 2. 每组只放 Score
    (city, scores) => new             // 3. 对每组生成统计结果
    {
        City = city,
        Count = scores.Count(),
        Avg = scores.Average(),
        Max = scores.Max(),
        Min = scores.Min()
    }
);

foreach (var s in stats)
    Console.WriteLine($"{s.City}: {s.Count}人, 均分{s.Avg:F1}, 最高{s.Max}, 最低{s.Min}");

输出:

北京: 3人, 均分87.7, 最高95, 最低76
上海: 2人, 均分86.5, 最高88, 最低85
广州: 2人, 均分51.5, 最高58, 最低45
深圳: 1人, 均分72.0, 最高72, 最低72

7.2 复合键分组

// 按城市+科目 两个条件分组
var multiGroup = students.GroupBy(s => new { s.City, s.Subject });

foreach (var group in multiGroup)
{
    Console.WriteLine($"--- {group.Key.City} - {group.Key.Subject} ({group.Count()}人) ---");
    foreach (var s in group)
        Console.WriteLine($"  {s.Name}: {s.Score}分");
}

输出(部分):

--- 北京 - 数学 (1人) ---
  张三: 92分
--- 北京 - 语文 (1人) ---
  王五: 76分
--- 北京 - 英语 (1人) ---
  周八: 95分
--- 上海 - 数学 (1人) ---
  李四: 85分
...

7.3 自定义条件分组

// 按分数段分组
var byLevel = students.GroupBy(s =>
{
    if (s.Score >= 90) return "A - 优秀";
    if (s.Score >= 80) return "B - 良好";
    if (s.Score >= 60) return "C - 及格";
    return "D - 不及格";
});

foreach (var group in byLevel)
{
    Console.WriteLine($"--- {group.Key} ({group.Count()}人) ---");
    foreach (var s in group)
        Console.WriteLine($"  {s.Name}: {s.Score}分");
}

输出:

--- A - 优秀 (2人) ---
  张三: 92分
  周八: 95分
--- B - 良好 (2人) ---
  李四: 85分
  孙七: 88分
--- C - 及格 (2人) ---
  王五: 76分
  郑十: 72分
--- D - 不及格 (2人) ---
  赵六: 58分
  吴九: 45分

7.4 分组后按组聚合值排序

// 按城市分组,按平均分降序排列
var ordered = students
    .GroupBy(s => s.City)
    .Select(g => new
    {
        City = g.Key,
        Count = g.Count(),
        Avg = g.Average(s => s.Score)
    })
    .OrderByDescending(x => x.Avg);

foreach (var item in ordered)
    Console.WriteLine($"{item.City}: {item.Count}人, 均分{item.Avg:F1}");

输出:

北京: 3人, 均分87.7
上海: 2人, 均分86.5
深圳: 1人, 均分72.0
广州: 2人, 均分51.5

八、查询语法中的分组(group by)

8.1 基本分组

// 查询语法
var byCity = from s in students
             group s by s.City;

// 等价方法语法
var byCity = students.GroupBy(s => s.City);

8.2 group by ... into —— 对分组结果继续处理

这是查询语法的大杀器——分组后继续筛选、排序、投影:

// 分组后,只要人数 ≥ 2 的组,按平均分降序
var result = from s in students
             group s by s.City into g           // 分组,结果存在 g 里
             where g.Count() >= 2               // 只要 2 人及以上的城市
             orderby g.Average(s => s.Score) descending  // 按均分降序
             select new
             {
                 City = g.Key,
                 Count = g.Count(),
                 Avg = g.Average(s => s.Score),
                 Max = g.Max(s => s.Score)
             };

foreach (var item in result)
    Console.WriteLine($"{item.City}: {item.Count}人, 均分{item.Avg:F1}, 最高{item.Max}");

输出:

北京: 3人, 均分87.7, 最高95
上海: 2人, 均分86.5, 最高88
广州: 2人, 均分51.5, 最高58

深圳只有 1 人,被 where g.Count() >= 2 筛掉了。

8.3 group by + into + let 组合

// 分组 → 计算及格率 → 只保留及格率 > 50% 的科目
var result = from s in students
             group s by s.Subject into g
             let passRate = (double)g.Count(s => s.Score >= 60) / g.Count()
             where passRate > 0.5
             orderby passRate descending
             select new
             {
                 Subject = g.Key,
                 Count = g.Count(),
                 PassRate = passRate.ToString("P0"),
                 Avg = g.Average(s => s.Score).ToString("F1")
             };

foreach (var item in result)
    Console.WriteLine($"{item.Subject}: {item.Count}人, 均分{item.Avg}, 及格率{item.PassRate}");

输出:

语文: 2人, 均分82.0, 及格率100%
数学: 4人, 均分76.8, 及格率75%

英语及格率 50%,被筛掉了。

8.4 查询语法 vs 方法语法——分组对比

// 同一需求的两种写法

// 查询语法:group by ... into 优雅
var r1 = from s in students
         group s by s.City into g
         where g.Count() >= 2
         select new { City = g.Key, Avg = g.Average(s => s.Score) };

// 方法语法:GroupBy + Select + Where
var r2 = students
    .GroupBy(s => s.City)
    .Select(g => new { City = g.Key, Avg = g.Average(s => s.Score) })
    .Where(x => x.Avg >= 70);

第九、ToLookup —— 立即分组

GroupBy 是延迟执行,ToLookup 是立即执行。

// ToLookup 立即分组,返回 ILookup<TKey, TElement>
ILookup<string, Student> lookup = students.ToLookup(s => s.City);

// 用起来像字典
foreach (var s in lookup["北京"])
    Console.WriteLine(s.Name);    // 张三, 王五, 周八

// 查不存在的 key 不报错,返回空序列
var empty = lookup["成都"];
Console.WriteLine(empty.Count());  // 0

// 对比 Dictionary:
Dictionary<string, List<Student>> dict;
// dict["成都"]  →  KeyNotFoundException!

GroupBy vs ToLookup:

特性 GroupBy ToLookup
执行时机 延迟执行 立即执行
返回类型 IEnumerable<IGrouping<K,T>> ILookup<K,T>
多次遍历 每次都重新分组 结果已固化
不存在的 key 不支持按 key 访问 返回空序列,不报错
适用场景 一次遍历 需要反复按 key 查询

十、联接 + 分组综合实战

10.1 完整示例:按省份统计学生成绩

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<Student> students = /* ... 测试数据 ... */;
        List<CityInfo> cities = /* ... 测试数据 ... */;

        Console.WriteLine("========== 按省份统计学生成绩 ==========\n");

        // 第1步:联接学生表和城市表
        // 第2步:按省份分组
        // 第3步:统计每组数据
        var byProvince = from s in students
                         join c in cities on s.City equals c.City
                         group new { s, c } by c.Province into g
                         orderby g.Average(x => x.s.Score) descending
                         select new
                         {
                             Province = g.Key,
                             Cities = g.Select(x => x.c.City).Distinct().Count(),
                             StudentCount = g.Count(),
                             AvgScore = g.Average(x => x.s.Score),
                             MaxScore = g.Max(x => x.s.Score),
                             MinScore = g.Min(x => x.s.Score),
                             PassRate = (double)g.Count(x => x.s.Score >= 60) / g.Count(),
                             TopStudent = g.OrderByDescending(x => x.s.Score).First().s.Name
                         };

        Console.WriteLine("{0,-6} {1,-6} {2,-6} {3,-8} {4,-6} {5,-6} {6,-8} {7}",
            "省份", "城市数", "人数", "均分", "最高", "最低", "及格率", "最佳学生");
        Console.WriteLine(new string('-', 65));

        foreach (var item in byProvince)
        {
            Console.WriteLine("{0,-6} {1,-6} {2,-6} {3,-8:F1} {4,-6} {5,-6} {6,-8:P0} {7}",
                item.Province, item.Cities, item.StudentCount,
                item.AvgScore, item.MaxScore, item.MinScore,
                item.PassRate, item.TopStudent);
        }
    }
}

输出:

========== 按省份统计学生成绩 ==========

省份   城市数 人数   均分     最高   最低   及格率   最佳学生
-----------------------------------------------------------------
上海   1      2      86.5     88     85     100%     孙七
北京   1      3      87.7     95     76     100%     周八
广东   2      3      58.3     72     45     33%      郑十

10.2 完整示例:多表联接 + 分组报表

// 三表联接 + 分组 + 排序
var fullReport = from s in students
                 join c in cities on s.City equals c.City
                 group new { s, c } by s.Subject into g
                 let passCount = g.Count(x => x.s.Score >= 60)
                 orderby g.Average(x => x.s.Score) descending
                 select new
                 {
                     科目 = g.Key,
                     人数 = g.Count(),
                     均分 = g.Average(x => x.s.Score),
                     最高 = g.Max(x => x.s.Score),
                     最低 = g.Min(x => x.s.Score),
                     及格人数 = passCount,
                     及格率 = $"{(double)passCount / g.Count():P0}",
                     城市分布 = string.Join(", ", g.Select(x => x.s.City).Distinct()),
                     省份 = string.Join(", ", g.Select(x => x.c.Province).Distinct())
                 };

Console.WriteLine("========== 科目综合报表 ==========\n");
Console.WriteLine("{0,-6} {1,-6} {2,-8} {3,-6} {4,-6} {5,-8} {6,-8} {7}",
    "科目", "人数", "均分", "最高", "最低", "及格人数", "及格率", "涉及省份");
Console.WriteLine(new string('-', 80));

foreach (var item in fullReport)
{
    Console.WriteLine("{0,-6} {1,-6} {2,-8:F1} {3,-6} {4,-6} {5,-8} {6,-8} {7}",
        item.科目, item.人数, item.均分, item.最高, item.最低,
        item.及格人数, item.及格率, item.省份);
}

输出:

========== 科目综合报表 ==========

科目   人数   均分     最高   最低   及格人数 及格率   涉及省份
-------------------------------------------------------------------------------
语文   2      82.0     88     76     2        100%     北京, 上海
数学   4      76.8     92     58     3        75%      北京, 上海, 广东
英语   2      70.0     95     45     1        50%      北京, 广东

十一、常见易错点(避坑指南)

坑1:Join 中 equals 不是 ==

// ❌ 错误语法
// from s in students
// join c in cities on s.City == c.City  // 编译错误!

// ✅ 正确语法
from s in students
join c in cities on s.City equals c.City

坑2:Join 后需要判断匹配不到的情况

// Join(内连接)只返回匹配的,不需要判空
var joined = from s in students
             join c in cities on s.City equals c.City
             select new { s.Name, c.Province };  // ✅ 省一定有值

// GroupJoin 要小心空组
var groupJoined = from c in cities
                  join s in students on c.City equals s.City into g
                  select new
                  {
                      c.City,
                      // AvgScore = g.Average(s => s.Score)  // ❌ 空组会抛异常!
                      AvgScore = g.Any() ? g.Average(s => s.Score) : (double?)null  // ✅
                  };

坑3:GroupBy 后 Aggregate 对空组的影响

// GroupBy 产生的组至少有一个元素,所以 Average 通常安全

// 但如果用 DefaultIfEmpty 就可能产生空组
var dangerous = students
    .Where(s => s.Score > 100)       // 没有 >100 的
    .GroupBy(s => s.City)            // 分组也没内容
    .Select(g => g.Average(s => s.Score));  // ❌ 可能抛异常

坑4:IGrouping 没有索引器

var byCity = students.GroupBy(s => s.City);

foreach (var group in byCity)
{
    var first = group.First();        // ✅ 可以
    // var bad = group[0];            // ❌ IGrouping 没有索引器!
    var second = group.ElementAt(1);  // ✅ 用 ElementAt
}

坑5:复合键分组后 Key 是匿名类型

var groups = students.GroupBy(s => new { s.City, s.Subject });

foreach (var g in groups)
{
    // g.Key 是匿名类型,里面有两个属性
    Console.WriteLine($"{g.Key.City} - {g.Key.Subject}");  // ✅
}

坑6:分组联接中的 into 不能和 select 互换位置

// ✅ 正确:join ... into 之后 select
from c in cities
join s in students on c.City equals s.City into g
select new { c.City, Count = g.Count() }

// ❌ 错误:into 后面不能直接接其他 join
// from c in cities
// join s in students on c.City equals s.City into g
// join x in table3 on c.X equals x.X  // 这样不行

十二、总结

联接速查

操作 语法(查询) 语法(方法) 说明
内连接 join c in cities on s.City equals c.City students.Join(cities, ...) 只要匹配的
分组联接 join s in students on c.City equals s.City into g cities.GroupJoin(students, ...) 左表全保留
多表连接 多个 join ... on ... equals 多次 .Join() 三表以上

分组速查

操作 语法(查询) 语法(方法) 说明
基本分组 group s by s.City .GroupBy(s => s.City) 按单字段分
复合键分组 group s by new { s.City, s.Subject } .GroupBy(s => new { ... }) 多条件分
自定义分组 group s by (s.Score >= 60 ? "及格" : "不及格") .GroupBy(s => 计算式) 按计算值分
分组后继续 group s by s.City into g where g.Count() >= 2 .GroupBy().Select().Where() 分组再处理
立即分组 (无查询语法直接支持) .ToLookup(s => s.City) 立即执行

记忆口诀

Join 联表配对找,内连只留匹配号
GroupJoin 左全保,空组也要列出来

GroupBy 分组归归类,相同键值聚一堆
into 后面继续筛,查询语法最爽快

复合键组用匿名,分组投影配合用
内连外连分清楚,数据报表一网捞

一句话总结Join 是按关联键把两张表拼在一起(内连接,只留匹配的);GroupJoin 是以左表为主,右表数据装进组里(像 LEFT JOIN,空组也保留);GroupBy 是把一张表按某个键拆成一堆一堆的,然后对每组做聚合统计。Join + GroupBy 组合是数据报表的黄金搭档。

0
博主关闭了当前页面的评论