Другое

Полное руководство: группировка по нескольким столбцам в LINQ

Узнайте, как выполнять группировку по нескольким столбцам в LINQ с использованием анонимных типов и синтаксиса запросов. Полное руководство с примерами и советами по производительности для операций GROUP BY, подобных SQL.

Как выполнить группировку по нескольким столбцам с помощью LINQ

Как выполнить операцию группировки по нескольким столбцам в LINQ, аналогично инструкции SQL GROUP BY с несколькими столбцами?

Например, в SQL я могу написать:

sql
SELECT * FROM <TableName> GROUP BY <Column1>,<Column2>

Или более конкретно:

sql
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 с лямбда-выражением, которое создает анонимный объект, содержащий все свойства, по которым вы хотите группировать:

csharp
var result = data.GroupBy(x => new { x.Column1, x.Column2 });

В синтаксисе запросов вы используете предложение group со схожей структурой анонимного объекта:

csharp
var result = from item in data
             group item by new { item.Column1, item.Column2 } into g
             select g;

Понимание структуры результата

Результат операции группировки по нескольким столбцам представляет собой коллекцию объектов IGrouping<TKey, TElement>, где TKey - это ваш анонимный тип, содержащий сгруппированные свойства:

csharp
foreach (var group in result)
{
    Console.WriteLine($"Ключ группы: {group.Key.Column1}, {group.Key.Column2}");
    Console.WriteLine($"Количество: {group.Count()}");
    
    foreach (var item in group)
    {
        // Доступ к отдельным элементам в группе
    }
}

Синтаксис запросов против методического синтаксиса

Оба синтаксиса - синтаксис запросов и методический синтаксис - дают одинаковый результат, но они предлагают разные преимущества читаемости в зависимости от сложности вашего запроса.

Примеры методического синтаксиса

Методический синтаксис часто более лаконичен и может быть легче для цепочки нескольких операций:

csharp
// Базовая группировка по нескольким столбцам
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)
    });

Примеры синтаксиса запросов

Синтаксис запросов может быть более читаемым для сложных запросов, особенно при работе с несколькими уровнями группировки:

csharp
// Базовая группировка
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 предоставляет различные агрегатные методы, которые работают безупречно с группировкой по нескольким столбцам.

Общие агрегатные методы

csharp
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)
    });

Несколько агрегатов в синтаксисе запросов

csharp
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)
                    };

Условные агрегаты

Иногда необходимо выполнять условные агрегации внутри групп:

csharp
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: Анализ данных о продажах

csharp
// Данные о продажах, сгруппированные по региону и категории продукта
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: Управление запасами

csharp
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: Анализ на основе времени

csharp
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

csharp
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 и выше можно использовать именованные кортежи:

csharp
// Использование именованных кортежей
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:

csharp
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)
                            }
                        };

Динамическая группировка

Когда столбцы для группировки должны определяться во время выполнения:

csharp
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);
}

Рекомендации по производительности

При работе с группировкой по нескольким столбцам учитывайте эти аспекты производительности для получения оптимальных результатов.

Индексация и оптимизация запросов

Для запросов к базе данных убедитесь в наличии правильных индексов по столбцам, используемым в группировке:

csharp
// Убедитесь, что таблицы базы данных имеют правильные индексы
// 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() });

Рекомендации по памяти для больших наборов данных

При работе с большими коллекциями в памяти:

csharp
// Для больших наборов данных рассмотрите потоковую обработку или постраничный вывод
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);
}

Кэширование результатов группировки

Для часто запрашиваемых данных группировки:

csharp
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. Ключевые подходы включают:

  1. Используйте анонимные типы (new { Column1, Column2 }) для создания составных ключей группировки
  2. Выбирайте между синтаксисом запросов и методическим синтаксисом на основе потребностей в читаемости
  3. Комбинируйте с агрегатными методами, такими как Sum, Count, Average для анализа данных
  4. Используйте кортежи C# 7.1+ для более чистого синтаксиса, когда это возможно
  5. Учитывайте последствия для производительности при работе с большими наборами данных и запросами к базе данных

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

Источники

  1. Группировка данных - C# | Microsoft Learn
  2. Как группировать по нескольким столбцам с помощью LINQ - Stack Overflow
  3. C# Linq Group By по нескольким столбцам - Stack Overflow
  4. LINQ Group By Multiple Columns - Sensible Dev
  5. LINQ Group By Multiple fields -Syntax help - Stack Overflow
  6. Как группировать по нескольким столбцам в LINQ - Educative
  7. Пример LINQ GroupBy C#: Как использовать Group by в LINQ Query - Web Training Room
  8. Как группировать по нескольким столбцам в запросах LINQ с использованием C# - Delft Stack
  9. Группировка и агрегирование данных с помощью LINQ - Pluralsight
  10. Linq: GroupBy, Sum and Count - Stack Overflow
Авторы
Проверено модерацией
Модерация