【C#】LINQのGroupByメソッドでデータをキーごとにグループ化する

業務アプリケーション開発において、売上データやログデータなどを特定の条件(カテゴリ、日付、ステータスなど)ごとに分類して処理したい場面は頻繁に発生します。

C#のLINQに含まれるGroupByメソッドを使用すると、こうしたグループ化処理を宣言的かつ簡潔に記述できます。ここでは、決済トランザクション履歴を「決済方法」ごとに分類し、それぞれの詳細を出力するシナリオを題材に、GroupByの基本的な使い方と特性について解説します。


目次

GroupByメソッドの概要

GroupByメソッドは、シーケンスの各要素に対して指定されたキーセレクタ関数を適用し、そのキーの値が等しい要素同士をまとめたグループのシーケンス(IEnumerable<IGrouping<TKey, TElement>>)を返します。

SQLのGROUP BY句に相当する機能ですが、LINQのGroupByは集計(SumやCount)を必須とせず、グループ化された階層構造そのものを扱うことができる点が特徴です。

実践的なコード例:決済方法別のトランザクション分類

以下のコードは、複数の決済履歴(クレジットカード、銀行振込、電子マネー)を含むリストを、決済方法ごとにグループ化して出力する例です。

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

namespace PaymentSystem
{
    // 決済方法を表す列挙型
    public enum PaymentMethod
    {
        CreditCard,
        BankTransfer,
        ElectronicMoney
    }

    // トランザクションデータを表すレコード
    public record Transaction(int Id, decimal Amount, PaymentMethod Method);

    class Program
    {
        static void Main()
        {
            // シナリオ:
            // 日々の売上トランザクションデータがある。
            // これらを「決済方法」ごとに分類して、内訳を確認したい。

            var transactions = new[]
            {
                new Transaction(1001, 5000m, PaymentMethod.CreditCard),
                new Transaction(1002, 1200m, PaymentMethod.ElectronicMoney),
                new Transaction(1003, 8000m, PaymentMethod.BankTransfer),
                new Transaction(1004, 3500m, PaymentMethod.CreditCard),
                new Transaction(1005, 600m,  PaymentMethod.ElectronicMoney),
                new Transaction(1006, 9000m, PaymentMethod.CreditCard),
            };

            // 1. GroupByを使用して決済方法(Method)をキーにグループ化
            // 戻り値は IEnumerable<IGrouping<PaymentMethod, Transaction>> となります。
            var groupedTransactions = transactions.GroupBy(t => t.Method);

            // 2. グループごとのデータを出力
            // 外側のループでグループ(キー)を処理
            foreach (var group in groupedTransactions)
            {
                Console.WriteLine($"# 決済方法: {group.Key}");

                // 内側のループでグループ内の各要素を処理
                foreach (var tx in group)
                {
                    Console.WriteLine($"  - Transaction ID: {tx.Id}, Amount: {tx.Amount:C}");
                }
                
                // (参考) グループ単位での集計も可能です
                Console.WriteLine($"  -> 合計金額: {group.Sum(x => x.Amount):C}\n");
            }
        }
    }
}

実行結果

# 決済方法: CreditCard
  - Transaction ID: 1001, Amount: ¥5,000
  - Transaction ID: 1004, Amount: ¥3,500
  - Transaction ID: 1006, Amount: ¥9,000
  -> 合計金額: ¥17,500

# 決済方法: ElectronicMoney
  - Transaction ID: 1002, Amount: ¥1,200
  - Transaction ID: 1005, Amount: ¥600
  -> 合計金額: ¥1,800

# 決済方法: BankTransfer
  - Transaction ID: 1003, Amount: ¥8,000
  -> 合計金額: ¥8,000

技術的なポイントとToLookupとの違い

1. IGroupingインターフェースの構造

GroupByの結果として得られる要素はIGrouping<TKey, TElement>インターフェースを実装しています。これは以下の特徴を持ちます。

  • Keyプロパティ: グループ化の基準となったキー値(上記の例ではPaymentMethod)。
  • IEnumerable<TElement>: そのグループに属する要素のリスト。それ自体が列挙可能です。

2. 遅延実行(Deferred Execution)

GroupByとよく似た機能にToLookupがありますが、実行タイミングに決定的な違いがあります。

  • GroupBy: 遅延実行されます。foreachで反復処理を行うまで、グループ化の処理は実行されません。
  • ToLookup: 即時実行されます。メソッドを呼び出した時点で全てのデータを読み込み、グループ化を完了させます。

データ量が膨大で、全てのグループを一度にメモリに乗せる必要がない場合や、ストリームのようにデータを処理したい場合はGroupByが適しています。逆に、同じグループ化結果に対して何度もランダムアクセスを行う場合はToLookupの方が効率的な場合があります。

3. 要素の射影(ElementSelector)

GroupByのオーバーロードを使用すると、グループ化と同時に要素の変換(射影)を行うことができます。例えば、オブジェクト全体ではなく「金額だけ」をリストにしたい場合は以下のように記述します。

// キーは決済方法、値は金額(decimal)のリストにする
var amountGroups = transactions.GroupBy(t => t.Method, t => t.Amount);

まとめ

GroupByメソッドは、データを構造化するための強力な手段です。フラットなリストデータを意味のある単位にまとめ上げることで、集計処理や階層的な表示が容易になります。「遅延実行である」という特性を理解し、ToLookupと適切に使い分けることで、より効率的なデータ処理を実装できます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次