Полное руководство: группировка по нескольким столбцам в LINQ
Узнайте, как выполнять группировку по нескольким столбцам в LINQ с использованием анонимных типов и синтаксиса запросов. Полное руководство с примерами и советами по производительности для операций GROUP BY, подобных SQL.
Как выполнить группировку по нескольким столбцам с помощью LINQ
Как выполнить операцию группировки по нескольким столбцам в LINQ, аналогично инструкции SQL GROUP BY с несколькими столбцами?
Например, в SQL я могу написать:
SELECT * FROM <TableName> GROUP BY <Column1>,<Column2>
Или более конкретно:
QuantityBreakdown
(
MaterialID int,
ProductID int,
Quantity float
)
INSERT INTO @QuantityBreakdown (MaterialID, ProductID, Quantity)
SELECT MaterialID, ProductID, SUM(Quantity)
FROM @Transactions
GROUP BY MaterialID, ProductID
Какой эквивалентный синтаксис LINQ позволяет достичь такой группировки по нескольким столбцам?
Группировка по нескольким столбцам с помощью LINQ
Для группировки по нескольким столбцам с помощью LINQ можно использовать анонимные типы в методе GroupBy или указывать несколько свойств в синтаксисе запросов. Эквивалентом GROUP BY Column1, Column2 в SQL является GroupBy(x => new { x.Column1, x.Column2 }) в методическом синтаксисе или group x by new { x.Column1, x.Column2 } в синтаксисе запросов, что позволяет выполнять агрегации, такие как Sum, Count и Average, по нескольким критериям группировки.
Содержание
- Начало работы с группировкой по нескольким столбцам
- Синтаксис запросов против методического синтаксиса
- Работа с агрегатами
- Практические примеры
- Продвинутые сценарии
- Рекомендации по производительности
Начало работы с группировкой по нескольким столбцам
Основной подход к группировке по нескольким столбцам в LINQ включает создание составного ключа с использованием анонимных типов. Этот составной ключ служит основой для группировки, аналогично тому, как SQL использует несколько столбцов в своем предложении GROUP BY.
Базовая структура синтаксиса
В методическом синтаксисе вы используете GroupBy с лямбда-выражением, которое создает анонимный объект, содержащий все свойства, по которым вы хотите группировать:
var result = data.GroupBy(x => new { x.Column1, x.Column2 });
В синтаксисе запросов вы используете предложение group со схожей структурой анонимного объекта:
var result = from item in data
group item by new { item.Column1, item.Column2 } into g
select g;
Понимание структуры результата
Результат операции группировки по нескольким столбцам представляет собой коллекцию объектов IGrouping<TKey, TElement>, где TKey - это ваш анонимный тип, содержащий сгруппированные свойства:
foreach (var group in result)
{
Console.WriteLine($"Ключ группы: {group.Key.Column1}, {group.Key.Column2}");
Console.WriteLine($"Количество: {group.Count()}");
foreach (var item in group)
{
// Доступ к отдельным элементам в группе
}
}
Синтаксис запросов против методического синтаксиса
Оба синтаксиса - синтаксис запросов и методический синтаксис - дают одинаковый результат, но они предлагают разные преимущества читаемости в зависимости от сложности вашего запроса.
Примеры методического синтаксиса
Методический синтаксис часто более лаконичен и может быть легче для цепочки нескольких операций:
// Базовая группировка по нескольким столбцам
var groupedData = dbContext.Products
.GroupBy(p => new { p.Category, p.SupplierID })
.Select(g => new
{
Category = g.Key.Category,
SupplierID = g.Key.SupplierID,
ProductCount = g.Count(),
TotalValue = g.Sum(p => p.Price * p.StockQuantity)
});
// С фильтрацией
var filteredGroups = dbContext.Orders
.Where(o => o.OrderDate >= DateTime.Now.AddDays(-30))
.GroupBy(o => new { o.CustomerID, o.ProductID })
.Where(g => g.Count() > 1)
.Select(g => new
{
CustomerID = g.Key.CustomerID,
ProductID = g.Key.ProductID,
OrderCount = g.Count(),
TotalAmount = g.Sum(o => o.Amount)
});
Примеры синтаксиса запросов
Синтаксис запросов может быть более читаемым для сложных запросов, особенно при работе с несколькими уровнями группировки:
// Базовая группировка
var groupedQuery = from order in dbContext.Orders
group order by new { order.CustomerID, order.ProductID } into g
select new
{
CustomerID = g.Key.CustomerID,
ProductID = g.Key.ProductID,
OrderCount = g.Count(),
TotalAmount = g.Sum(o => o.Amount)
};
// Вложенная группировка
var nestedGroups = from student in dbContext.Students
group student by student.Year into yearGroup
from nameGroup in yearGroup
.GroupBy(s => s.LastName)
select new
{
Year = yearGroup.Key,
LastName = nameGroup.Key,
StudentCount = nameGroup.Count()
};
Выбор между синтаксисами
- Методический синтаксис предпочтителен для простых, цепочечных операций и при работе с цепочкой методов
- Синтаксис запросов часто обеспечивает лучшую читаемость для сложных запросов с несколькими предложениями
- Смешанный синтаксис может использоваться для объединения преимуществ обоих подходов
Работа с агрегатами
При группировке по нескольким столбцам вы обычно захотите выполнять агрегатные вычисления для каждой группы. LINQ предоставляет различные агрегатные методы, которые работают безупречно с группировкой по нескольким столбцам.
Общие агрегатные методы
var summary = dbContext.Transactions
.GroupBy(t => new { t.MaterialID, t.ProductID })
.Select(g => new
{
MaterialID = g.Key.MaterialID,
ProductID = g.Key.ProductID,
TotalQuantity = g.Sum(t => t.Quantity),
AveragePrice = g.Average(t => t.UnitPrice),
TransactionCount = g.Count(),
MaxQuantity = g.Max(t => t.Quantity),
MinQuantity = g.Min(t => t.Quantity)
});
Несколько агрегатов в синтаксисе запросов
var aggregateQuery = from trans in dbContext.Transactions
group trans by new { trans.MaterialID, trans.ProductID } into g
select new
{
g.Key.MaterialID,
g.Key.ProductID,
TotalSum = g.Sum(t => t.Quantity * t.UnitPrice),
AverageQuantity = g.Average(t => t.Quantity),
TransactionCount = g.Count(),
FirstTransactionDate = g.Min(t => t.TransactionDate),
LastTransactionDate = g.Max(t => t.TransactionDate)
};
Условные агрегаты
Иногда необходимо выполнять условные агрегации внутри групп:
var conditionalAggregates = dbContext.Orders
.GroupBy(o => new { o.CustomerID, o.ProductID })
.Select(g => new
{
CustomerID = g.Key.CustomerID,
ProductID = g.Key.ProductID,
TotalOrders = g.Count(),
CompletedOrders = g.Count(o => o.Status == "Completed"),
PendingOrders = g.Count(o => o.Status == "Pending"),
TotalRevenue = g.Sum(o => o.Status == "Completed" ? o.Amount : 0),
AverageProcessingTime = g.Where(o => o.ProcessingTime.HasValue)
.Average(o => o.ProcessingTime.Value)
});
Практические примеры
Рассмотрим несколько практических сценариев, демонстрирующих группировку по нескольким столбцам в реальных приложениях.
Пример 1: Анализ данных о продажах
// Данные о продажах, сгруппированные по региону и категории продукта
var salesByRegionAndCategory = dbContext.Sales
.GroupBy(s => new { s.Region, s.ProductCategory })
.Select(g => new
{
Region = g.Key.Region,
ProductCategory = g.Key.ProductCategory,
TotalSales = g.Sum(s => s.Amount),
AverageSale = g.Average(s => s.Amount),
SaleCount = g.Count(),
TopCustomer = g.OrderByDescending(s => s.Amount).FirstOrDefault().CustomerName
})
.OrderByDescending(x => x.TotalSales);
// SQL эквивалент:
// SELECT Region, ProductCategory,
// SUM(Amount) as TotalSales, AVG(Amount) as AverageSale,
// COUNT(*) as SaleCount, MAX(CASE WHEN Amount = MAX(Amount) OVER (PARTITION BY Region, ProductCategory) THEN CustomerName END) as TopCustomer
// FROM Sales
// GROUP BY Region, ProductCategory
// ORDER BY TotalSales DESC
Пример 2: Управление запасами
var inventoryBreakdown = dbContext.InventoryItems
.GroupBy(i => new { i.MaterialID, i.ProductID })
.Select(g => new
{
MaterialID = g.Key.MaterialID,
ProductID = g.Key.ProductID,
TotalQuantity = g.Sum(i => i.Quantity),
AverageUnitCost = g.Average(i => i.UnitCost),
TotalValue = g.Sum(i => i.Quantity * i.UnitCost),
Locations = g.Select(i => i.Location).Distinct().ToList(),
LastUpdated = g.Max(i => i.LastUpdatedDate)
});
// Версия в синтаксисе запросов
var inventoryQuery = from item in dbContext.InventoryItems
group item by new { item.MaterialID, item.ProductID } into g
select new InventorySummary
{
MaterialID = g.Key.MaterialID,
ProductID = g.Key.ProductID,
TotalQuantity = g.Sum(i => i.Quantity),
AverageUnitCost = g.Average(i => i.UnitCost),
TotalValue = g.Sum(i => i.Quantity * i.UnitCost),
Locations = g.Select(i => i.Location).Distinct().ToList(),
LastUpdated = g.Max(i => i.LastUpdatedDate)
};
Пример 3: Анализ на основе времени
var monthlySales = dbContext.Orders
.Where(o => o.OrderDate >= DateTime.Now.AddMonths(-6))
.GroupBy(o => new
{
o.OrderDate.Year,
o.OrderDate.Month,
o.CustomerID
})
.Select(g => new
{
Year = g.Key.Year,
Month = g.Key.Month,
CustomerID = g.Key.CustomerID,
MonthlyTotal = g.Sum(o => o.Amount),
OrderCount = g.Count(),
AverageOrderValue = g.Average(o => o.Amount),
FirstOrderDate = g.Min(o => o.OrderDate),
LastOrderDate = g.Max(o => o.OrderDate)
})
.OrderBy(x => x.Year)
.ThenBy(x => x.Month);
Продвинутые сценарии
Для более сложных требований LINQ предоставляет несколько продвинутых техник для группировки по нескольким столбцам.
Комбинирование с другими операциями LINQ
var complexAnalysis = dbContext.Transactions
.Where(t => t.TransactionDate >= DateTime.Now.AddDays(-90))
.GroupBy(t => new { t.MaterialID, t.ProductID })
.Select(g => new
{
g.Key.MaterialID,
g.Key.ProductID,
TotalQuantity = g.Sum(t => t.Quantity),
RecentTransactions = g
.Where(t => t.TransactionDate >= DateTime.Now.AddDays(-30))
.Count(),
HighValueTransactions = g
.Where(t => t.UnitPrice > 100)
.Sum(t => t.Quantity * t.UnitPrice),
PriceTrend = g.Any() ?
(g.Max(t => t.UnitPrice) - g.Min(t => t.UnitPrice)) / g.Average(t => t.UnitPrice) : 0
})
.Where(x => x.TotalQuantity > 100 || x.RecentTransactions > 5);
Использование кортежей (C# 7.1+)
Для более чистого синтаксиса с C# 7.1 и выше можно использовать именованные кортежи:
// Использование именованных кортежей
var result = dbContext.Products
.GroupBy(p => (p.Category, p.SupplierID))
.Select(g => new
{
Category = g.Key.Category,
SupplierID = g.Key.SupplierID,
ProductCount = g.Count()
});
// Использование позиционных кортежей
var positionalResult = dbContext.Products
.GroupBy(p => (p.CategoryID, p.SupplierID))
.Select(g => new
{
CategoryID = g.Key.Item1,
SupplierID = g.Key.Item2,
Count = g.Count()
});
Иерархическая группировка
Для многоуровневой группировки, аналогичной операциям ROLLUP или CUBE в SQL:
var hierarchicalGroups = from order in dbContext.Orders
group order by order.CustomerID into customerGroup
from productGroup in customerGroup
.GroupBy(o => o.ProductID)
select new
{
CustomerID = customerGroup.Key,
ProductGroup = new
{
ProductID = productGroup.Key,
OrderCount = productGroup.Count(),
TotalAmount = productGroup.Sum(o => o.Amount)
},
CustomerTotal = new
{
TotalOrders = customerGroup.Count(),
CustomerAmount = customerGroup.Sum(o => o.Amount)
}
};
Динамическая группировка
Когда столбцы для группировки должны определяться во время выполнения:
public dynamic GetDynamicGrouping<T>(IEnumerable<T> data, string[] groupProperties)
{
// Создание динамического ключа группировки
var parameter = Expression.Parameter(typeof(T));
var bindings = groupProperties.Select(prop =>
Expression.Bind(
Expression.Property(parameter, prop),
Expression.Property(parameter, prop)
)
).ToArray();
var keyType = Expression.New(typeof(object));
var keyLambda = Expression.Lambda(
Expression.New(keyType, bindings.Select(b => b.Expression), bindings.Select(b => b.Member)),
parameter
);
return data.AsQueryable().GroupBy(keyLambda);
}
Рекомендации по производительности
При работе с группировкой по нескольким столбцам учитывайте эти аспекты производительности для получения оптимальных результатов.
Индексация и оптимизация запросов
Для запросов к базе данных убедитесь в наличии правильных индексов по столбцам, используемым в группировке:
// Убедитесь, что таблицы базы данных имеют правильные индексы
// CREATE INDEX IX_Transactions_MaterialID_ProductID ON Transactions(MaterialID, ProductID);
// В LINQ это преобразуется в:
var optimizedQuery = dbContext.Transactions
.Where(t => t.MaterialID > 0) // Сначала фильтруйте, чтобы уменьшить набор данных
.GroupBy(t => new { t.MaterialID, t.ProductID })
.Select(g => new { g.Key.MaterialID, g.Key.ProductID, Count = g.Count() });
Рекомендации по памяти для больших наборов данных
При работе с большими коллекциями в памяти:
// Для больших наборов данных рассмотрите потоковую обработку или постраничный вывод
var largeDatasetGroups = dbContext.LargeDataset
.AsNoTracking() // Отключите отслеживание изменений для лучшей производительности
.GroupBy(x => new { x.Column1, x.Column2 })
.Select(g => new { g.Key.Column1, g.Key.Column2, Count = g.Count() })
.ToList(); // Материализуйте результаты
// Или обрабатывайте пакетами
const int batchSize = 1000;
var allGroups = new List<GroupResult>();
for (int i = 0; i < dbContext.LargeDataset.Count(); i += batchSize)
{
var batch = dbContext.LargeDataset.Skip(i).Take(batchSize);
var batchGroups = batch.GroupBy(x => new { x.Column1, x.Column2 })
.Select(g => new GroupResult { /* ... */ })
.ToList();
allGroups.AddRange(batchGroups);
}
Кэширование результатов группировки
Для часто запрашиваемых данных группировки:
public class GroupingCache
{
private static readonly ConcurrentDictionary<string, object> _cache = new();
public static List<TGroup> GetOrAddGrouping<T, TGroup>(
string cacheKey,
IEnumerable<T> data,
Func<IEnumerable<T>, IEnumerable<TGroup>> groupingFunc)
{
return (List<TGroup>)_cache.GetOrAdd(cacheKey, _ =>
groupingFunc(data).ToList());
}
}
// Использование
var cachedGroups = GroupingCache.GetOrAddGrouping(
"sales_by_category_region",
dbContext.Sales,
sales => sales.GroupBy(s => new { s.Category, s.Region })
.Select(g => new { g.Key.Category, g.Key.Region, Count = g.Count() })
.ToList());
Заключение
Группировка по нескольким столбцам в LINQ - это мощная техника, которая имитирует функциональность SQL GROUP BY, сохраняя при этом безопасность типов и поддержку IntelliSense. Ключевые подходы включают:
- Используйте анонимные типы (
new { Column1, Column2 }) для создания составных ключей группировки - Выбирайте между синтаксисом запросов и методическим синтаксисом на основе потребностей в читаемости
- Комбинируйте с агрегатными методами, такими как Sum, Count, Average для анализа данных
- Используйте кортежи C# 7.1+ для более чистого синтаксиса, когда это возможно
- Учитывайте последствия для производительности при работе с большими наборами данных и запросами к базе данных
Независимо от того, анализируете ли вы данные о продажах, управляете запасами или выполняете анализ на основе времени, группировка по нескольким столбцам обеспечивает основу для комплексной агрегации и анализа данных в ваших запросах LINQ.
Источники
- Группировка данных - C# | Microsoft Learn
- Как группировать по нескольким столбцам с помощью LINQ - Stack Overflow
- C# Linq Group By по нескольким столбцам - Stack Overflow
- LINQ Group By Multiple Columns - Sensible Dev
- LINQ Group By Multiple fields -Syntax help - Stack Overflow
- Как группировать по нескольким столбцам в LINQ - Educative
- Пример LINQ GroupBy C#: Как использовать Group by в LINQ Query - Web Training Room
- Как группировать по нескольким столбцам в запросах LINQ с использованием C# - Delft Stack
- Группировка и агрегирование данных с помощью LINQ - Pluralsight
- Linq: GroupBy, Sum and Count - Stack Overflow