Group by в LINQ: 5 вопросов, которые мучают новичков

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Group by в LINQ: разбираемся с группировкой на реальных примерах

Если вы хоть раз писали запросы к коллекциям на C#, то наверняка сталкивались с задачей: «А как мне сгруппировать данные?». Тема group by in LINQ — одна из самых популярных на Stack Overflow (десятки тысяч просмотров). И это не случайно: группировка кажется простой только на первый взгляд. На деле новички спотыкаются о типичные грабли: как выбрать первую запись в группе, как посчитать сумму или сделать вложенную группировку.

В этой статье мы разберём 5 самых частых вопросов про LINQ GroupBy и дадим работающие ответы. Поехали!

1. Как работает GroupBy в LINQ? Базовый синтаксис

Метод GroupBy — это аналог SQL-оператора GROUP BY. Он разбивает последовательность на группы по заданному ключу. Результат — коллекция объектов IGrouping<TKey, TElement>, где каждый объект содержит ключ и все элементы, попавшие в эту группу.

Вот простейший пример. Допустим, у нас есть список студентов:

public class Student
{
    public string Name { get; set; }
    public string Group { get; set; }
    public int Score { get; set; }
}

var students = new List<Student>
{
    new Student { Name = "Анна", Group = "A", Score = 85 },
    new Student { Name = "Борис", Group = "B", Score = 90 },
    new Student { Name = "Вика", Group = "A", Score = 78 },
    new Student { Name = "Глеб", Group = "B", Score = 92 }
};

Группируем по названию группы:

var grouped = students.GroupBy(s => s.Group);

foreach (var group in grouped)
{
    Console.WriteLine($"Группа: {group.Key}");
    foreach (var student in group)
    {
        Console.WriteLine($"  {student.Name} — {student.Score}");
    }
}

Вывод:

Группа: A
  Анна — 85
  Вика — 78
Группа: B
  Борис — 90
  Глеб — 92

Всё просто. Но дьявол, как всегда, в деталях.

2. Как выбрать первую запись из каждой группы?

Это — самый популярный вопрос по group by in LINQ. Допустим, нам нужно получить одного лучшего студента из каждой группы (с максимальным баллом). Как это сделать?

Решение — комбинировать GroupBy с First() или OrderByDescending:

var bestStudents = students
    .GroupBy(s => s.Group)
    .Select(g => g.OrderByDescending(s => s.Score).First());

foreach (var student in bestStudents)
{
    Console.WriteLine($"{student.Name} — {student.Group} — {student.Score}");
}

Результат: Анна (85) и Глеб (92). Обратите внимание: внутри Select мы сортируем группу и берём первый элемент. Это и есть ответ на вопрос «как сделать LINQ group by first».

Важно: First() выбросит исключение, если группа пуста. Если такое возможно — используйте FirstOrDefault().

3. Как посчитать сумму, количество или среднее по группе?

Часто после группировки нужно не просто перебрать элементы, а получить агрегированные значения. Например, сумма баллов по каждой группе или средний балл.

Используем Sum, Count, Average:

var groupStats = students
    .GroupBy(s => s.Group)
    .Select(g => new
    {
        Group = g.Key,
        Count = g.Count(),
        TotalScore = g.Sum(s => s.Score),
        AverageScore = g.Average(s => s.Score)
    });

foreach (var stat in groupStats)
{
    Console.WriteLine($"Группа {stat.Group}: {stat.Count} студентов, сумма {stat.TotalScore}, средний {stat.AverageScore:F2}");
}

Вывод:

Группа A: 2 студентов, сумма 163, средний 81.50
Группа B: 2 студентов, сумма 182, средний 91.00

Также можно использовать Min и Max — всё как в SQL.

4. Как сделать группировку по нескольким полям (multiple keys)?

Бывает, что ключ группировки состоит из нескольких свойств. Например, нужно сгруппировать студентов не только по группе, но и по году поступления. В LINQ group by multiple keys решается через анонимный тип:

var studentsWithYear = students.Select(s => new
{
    s.Name,
    s.Group,
    s.Score,
    Year = 2024 // допустим, все поступили в 2024
});

var multiGrouped = studentsWithYear
    .GroupBy(s => new { s.Group, s.Year })
    .Select(g => new
    {
        g.Key.Group,
        g.Key.Year,
        Count = g.Count()
    });

foreach (var item in multiGrouped)
{
    Console.WriteLine($"Группа {item.Group}, год {item.Year}: {item.Count} студентов");
}

Ключ — анонимный объект с нужными полями. C# сам сравнивает такие объекты по значениям всех свойств, поэтому группировка работает корректно.

Совет: если полей больше двух, лучше вынести ключ в отдельный класс или record — код станет читаемее.

5. Как отсортировать группы после группировки?

После GroupBy порядок групп не гарантирован. Чтобы его задать, добавьте OrderBy (или ThenBy):

var sortedGroups = students
    .GroupBy(s => s.Group)
    .OrderBy(g => g.Key) // по алфавиту
    .Select(g => new
    {
        Group = g.Key,
        Students = g.OrderByDescending(s => s.Score).ToList()
    });

foreach (var group in sortedGroups)
{
    Console.WriteLine($"Группа {group.Group}:");
    foreach (var student in group.Students)
    {
        Console.WriteLine($"  {student.Name} — {student.Score}");
    }
}

Можно сортировать не по ключу, а по агрегату: OrderBy(g => g.Sum(s => s.Score)) — чтобы сначала шли группы с наименьшей суммой баллов.

Бонус: GroupBy vs ToLookup — в чём разница?

Новички часто путают GroupBy с ToLookup. Оба метода группируют данные. Но:

  • GroupBy — отложенное выполнение (lazy). Результат вычисляется при переборе.
  • ToLookup — немедленное выполнение (eager). Создаёт структуру данных, к которой можно обращаться по ключу через индексатор.

Пример с ToLookup:

var lookup = students.ToLookup(s => s.Group);
var groupA = lookup["A"]; // сразу получаем всех студентов                

Блоги

Рекомендаций по книгам