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"]; // сразу получаем всех студентов