業務アプリケーション開発において、売上データやログデータなどを特定の条件(カテゴリ、日付、ステータスなど)ごとに分類して処理したい場面は頻繁に発生します。
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と適切に使い分けることで、より効率的なデータ処理を実装できます。
