【C#】LINQ集計テクニック大全:Count・Sum・GroupBy・Aggregateを実例で網羅
LINQの集計はCount
,Sum
,Average
,Min
,Max
で単純統計を一行で取り、GroupBy
と組み合わせればカテゴリ別集計も容易に扱えます。
複雑な要約はAggregate
で自由に計算でき、ネイティブループより短く可読性が高く、遅延実行でメモリ効率も保てます。
LINQ集計の基本
LINQ(Language Integrated Query)は、C#でデータの集計や操作を簡潔に記述できる強力な機能です。
LINQの集計メソッドを使うことで、配列やリスト、データベースのクエリ結果など、さまざまなデータソースから効率的に情報を抽出できます。
ここでは、LINQの集計に関する基本的な考え方や特徴を解説いたします。
集計メソッド共通の書式と流れ
LINQの集計メソッドは、主にIEnumerable<T>
やIQueryable<T>
の拡張メソッドとして提供されています。
代表的な集計メソッドにはCount
、Sum
、Min
、Max
、Average
、Aggregate
などがあります。
これらは、シーケンス(コレクション)の要素を対象に集計処理を行い、単一の結果を返します。
集計メソッドの基本的な書式は以下のようになります。
var result = source.AggregateMethod(selector);
source
は集計対象のシーケンス(例:配列やリスト)AggregateMethod
はCount
やSum
などの集計メソッドselector
は要素から集計対象の値を抽出するためのラムダ式(省略可能)
例えば、整数の配列から偶数の数を数える場合は以下のように書きます。
int[] numbers = { 1, 2, 3, 4, 5, 6 };
int evenCount = numbers.Count(n => n % 2 == 0);
この例では、Count
メソッドに条件を指定することで、偶数の要素数を取得しています。
集計メソッドの流れは以下の通りです。
- シーケンスの各要素に対して条件や変換を適用(必要に応じて)
- 条件を満たす要素を抽出または変換
- 集計処理を実行し、単一の結果を返す
この流れはどの集計メソッドでも共通しており、シンプルなコードで複雑な集計処理を実現できます。
IEnumerable と IQueryable の違いが及ぼす影響
LINQの集計メソッドは、IEnumerable<T>
とIQueryable<T>
の両方で利用可能ですが、これらのインターフェースの違いによって動作やパフォーマンスに影響が出ます。
- IEnumerable<T>
メモリ上のコレクションに対してLINQを適用する場合に使います。
LINQの処理はC#のコードとして実行され、すべての要素がメモリに読み込まれた状態で処理されます。
例:配列、リスト、メモリ内のデータ
- IQueryable<T>
データベースやリモートのデータソースに対してLINQを適用する場合に使います。
LINQのクエリは式ツリーとして表現され、実際の処理はデータソース側(例:SQLサーバー)で実行されます。
例:Entity FrameworkのDbSet、LINQ to SQL
この違いにより、集計メソッドの実行タイミングやパフォーマンスが変わります。
具体例
// IEnumerableの場合(メモリ内の配列)
int[] numbers = { 1, 2, 3, 4, 5 };
int count = numbers.Count(n => n > 2); // 条件に合う要素数をカウント
// IQueryableの場合(データベースのテーブルを想定)
IQueryable<Person> people = dbContext.People;
int adultCount = people.Count(p => p.Age >= 20); // SQLで実行される
IEnumerable
の場合は、すべての要素がメモリに読み込まれ、C#のコードで条件判定が行われます。
一方、IQueryable
の場合は、条件がSQLクエリに変換され、データベース側で集計が実行されるため、効率的に処理できます。
遅延実行と即時実行が集計に与える作用
LINQのクエリは大きく分けて「遅延実行」と「即時実行」の2種類があります。
集計メソッドは基本的に即時実行されるため、この違いを理解しておくことが重要です。
- 遅延実行
クエリの定義はすぐに実行されず、実際に結果が必要になったタイミングで初めて処理が行われます。
Where
やSelect
などのメソッドは遅延実行の代表例です。
- 即時実行
クエリの結果をすぐに計算し、結果を返します。
Count
、Sum
、ToList
、Aggregate
などのメソッドは即時実行です。
遅延実行の例
int[] numbers = { 1, 2, 3, 4, 5 };
var query = numbers.Where(n => n > 2); // まだ実行されていない
int count = query.Count(); // ここで初めて実行される
Where
で条件を指定しただけでは処理は実行されません。
Count
が呼ばれた時点で、Where
の条件に合う要素を数える処理が実行されます。
即時実行の例
int[] numbers = { 1, 2, 3, 4, 5 };
int sum = numbers.Sum(); // すぐに合計が計算される
Sum
は呼び出された時点で合計値を計算し、結果を返します。
集計メソッドの即時実行がもたらすメリット
- 集計結果がすぐに得られるため、後続の処理にすぐ使える
- 遅延実行のクエリと組み合わせることで、効率的に必要なデータだけを処理できる
注意点
遅延実行のクエリに対して複数回集計メソッドを呼ぶと、その都度処理が実行されるため、パフォーマンスに影響が出ることがあります。
必要に応じてToList
やToArray
で結果をキャッシュすることも検討してください。
以上がLINQ集計の基本的な考え方です。
集計メソッドの共通の書式やIEnumerable
とIQueryable
の違い、遅延実行と即時実行の特徴を理解することで、LINQをより効果的に活用できるようになります。
要素数を扱う集計
Count と LongCount の使い分け
Count
メソッドはLINQで最もよく使われる集計メソッドの一つで、シーケンス内の要素数を取得します。
ただし、要素数が非常に大きい場合はCount
の戻り値であるint
型の範囲を超える可能性があります。
そうした場合に備えて、LongCount
メソッドが用意されています。
Count
は戻り値がint
型(最大約21億)LongCount
は戻り値がlong
型(最大約9京)
通常のコレクションやデータセットであればCount
で十分ですが、ビッグデータや大規模なログ解析など、要素数がint.MaxValue
を超える可能性がある場合はLongCount
を使うべきです。
条件付きカウントで部分集合を抽出
Count
やLongCount
は条件を指定して、特定の条件を満たす要素だけをカウントできます。
条件はラムダ式で記述し、シーケンスの各要素に対して真偽判定を行います。
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 偶数の数をカウント
int evenCount = numbers.Count(n => n % 2 == 0);
Console.WriteLine($"偶数の数: {evenCount}");
偶数の数: 5
この例では、Count
に条件式n => n % 2 == 0
を渡すことで、偶数の要素だけを数えています。
LongCount
も同様に条件付きで使えます。
long largeCount = numbers.LongCount(n => n > 5);
Console.WriteLine($"5より大きい数の数: {largeCount}");
5より大きい数の数: 5
ラムダ式とデリゲート式の比較応用
Count
やLongCount
の条件にはラムダ式を使うのが一般的ですが、デリゲートFunc<T, bool>
を変数として用意し、再利用することも可能です。
これにより、条件を動的に切り替えたり、複数の集計で同じ条件を使い回したりできます。
Func<int, bool> isOdd = n => n % 2 != 0;
int oddCount = numbers.Count(isOdd);
Console.WriteLine($"奇数の数: {oddCount}");
奇数の数: 5
また、条件をメソッドとして定義し、デリゲートとして渡すこともできます。
static bool IsMultipleOfThree(int n) => n % 3 == 0;
int multipleOfThreeCount = numbers.Count(IsMultipleOfThree);
Console.WriteLine($"3の倍数の数: {multipleOfThreeCount}");
3の倍数の数: 3
このように、ラムダ式とデリゲートはほぼ同じ使い方ができますが、デリゲートを変数やメソッドとして切り出すことでコードの再利用性が高まります。
Any・All・Contains による存在判定とブール集計
要素数の集計とは少し異なりますが、LINQには要素の存在判定や条件の全件判定を行うメソッドもあります。
これらはブール値を返し、条件に合う要素があるかどうか、またはすべての要素が条件を満たすかどうかを判定します。
Any
:条件を満たす要素が1つでもあればtrue
を返すAll
:すべての要素が条件を満たす場合にtrue
を返すContains
:指定した値がシーケンスに含まれているか判定する
Any の使い方
bool hasEven = numbers.Any(n => n % 2 == 0);
Console.WriteLine($"偶数が含まれているか: {hasEven}");
偶数が含まれているか: True
Any
は条件を満たす要素が見つかった時点で処理を終了するため、効率的に存在判定ができます。
条件を省略すると、空でないかどうかの判定になります。
bool hasElements = numbers.Any();
Console.WriteLine($"要素が1つでもあるか: {hasElements}");
要素が1つでもあるか: True
All の使い方
bool allPositive = numbers.All(n => n > 0);
Console.WriteLine($"すべての要素が正の数か: {allPositive}");
すべての要素が正の数か: True
All
は条件を満たさない要素が見つかった時点でfalse
を返します。
すべての要素が条件を満たすかどうかを簡単にチェックできます。
Contains の使い方
bool containsFive = numbers.Contains(5);
Console.WriteLine($"5が含まれているか: {containsFive}");
5が含まれているか: True
Contains
は指定した値がシーケンスに含まれているかを判定します。
内部的にはEquals
メソッドを使って比較します。
まとめて使う例
if (numbers.Any(n => n > 8))
{
Console.WriteLine("9以上の数が含まれています。");
}
if (numbers.All(n => n < 20))
{
Console.WriteLine("すべての数は20未満です。");
}
if (numbers.Contains(3))
{
Console.WriteLine("3がリストにあります。");
}
9以上の数が含まれています。
すべての数は20未満です。
3がリストにあります。
これらのメソッドは、条件に合う要素の有無を素早く判定したい場合に非常に便利です。
特にAny
は大規模データの中で条件を満たす要素が早期に見つかれば処理を打ち切るため、パフォーマンス向上にも寄与します。
数値統計メソッド
Sum で合計を求める基本パターン
Sum
メソッドはLINQで数値の合計を求める際に使います。
整数型や浮動小数点型のシーケンスに対して簡単に合計値を取得できます。
基本的な使い方は以下の通りです。
int[] numbers = { 1, 2, 3, 4, 5 };
int total = numbers.Sum();
Console.WriteLine($"合計: {total}");
合計: 15
この例では、numbers
配列のすべての要素を足し合わせて合計を計算しています。
Sum
は整数型、double
型、decimal
型など、さまざまな数値型に対応しています。
また、オブジェクトのリストから特定の数値プロパティの合計を求める場合は、Sum
にラムダ式を渡します。
var items = new[]
{
new { Name = "A", Price = 100 },
new { Name = "B", Price = 200 },
new { Name = "C", Price = 300 }
};
int totalPrice = items.Sum(item => item.Price);
Console.WriteLine($"合計金額: {totalPrice}");
合計金額: 600
型変換が必要な場合の書き方
Sum
は対象の型に応じてオーバーロードされていますが、数値以外の型や計算結果を別の型で扱いたい場合は、明示的に型変換を行う必要があります。
例えば、int
の合計をlong
で扱いたい場合は、ラムダ式内でキャストします。
int[] numbers = { int.MaxValue, int.MaxValue };
long totalLong = numbers.Sum(n => (long)n);
Console.WriteLine($"long型での合計: {totalLong}");
long型での合計: 4294967294
このように、Sum
の戻り値は元の型に依存するため、オーバーフローを防ぐために型変換を行うことが重要です。
また、decimal
型の合計を求める場合も同様に、対象の値をdecimal
に変換してから合計を計算します。
float[] prices = { 10.5f, 20.3f, 30.2f };
decimal totalDecimal = prices.Sum(p => (decimal)p);
Console.WriteLine($"decimal型での合計: {totalDecimal}");
decimal型での合計: 61.0
Average で平均値を取得する際の注意点
Average
メソッドはシーケンスの平均値を計算します。
Sum
と同様に数値型に対応しており、オブジェクトのプロパティから平均を求めることも可能です。
double[] values = { 1.5, 2.5, 3.5 };
double avg = values.Average();
Console.WriteLine($"平均値: {avg}");
平均値: 2.5
丸め誤差と小数部の扱い
Average
は浮動小数点数の計算を行うため、丸め誤差が発生することがあります。
特にfloat
やdouble
型では、計算結果の小数部が微妙に異なる場合があるため注意が必要です。
float[] values = { 0.1f, 0.2f, 0.3f };
float avgFloat = values.Average();
Console.WriteLine($"float型の平均値: {avgFloat}");
float型の平均値: 0.2
このように、計算結果が期待値とわずかに異なることがあります。
精度が重要な場合はdecimal
型を使うか、計算後に丸め処理を行うことを検討してください。
decimal[] valuesDecimal = { 0.1m, 0.2m, 0.3m };
decimal avgDecimal = valuesDecimal.Average();
Console.WriteLine($"decimal型の平均値: {avgDecimal}");
decimal型の平均値: 0.2
また、Average
は空のシーケンスに対して呼び出すと例外が発生します。
空の場合の対策としては、DefaultIfEmpty
を使ってデフォルト値を設定する方法があります。
int[] empty = { };
double avgSafe = empty.DefaultIfEmpty(0).Average();
Console.WriteLine($"空シーケンスの平均値(デフォルト0): {avgSafe}");
空シーケンスの平均値(デフォルト0): 0
Min・Max で範囲を把握する
Min
とMax
はシーケンスの最小値と最大値を取得します。
数値だけでなく、文字列や日付など比較可能な型にも対応しています。
int[] numbers = { 5, 3, 9, 1, 7 };
int minValue = numbers.Min();
int maxValue = numbers.Max();
Console.WriteLine($"最小値: {minValue}, 最大値: {maxValue}");
最小値: 1, 最大値: 9
オブジェクトのプロパティに対しても同様に使えます。
var products = new[]
{
new { Name = "A", Price = 100 },
new { Name = "B", Price = 200 },
new { Name = "C", Price = 150 }
};
int minPrice = products.Min(p => p.Price);
int maxPrice = products.Max(p => p.Price);
Console.WriteLine($"最安値: {minPrice}, 最高値: {maxPrice}");
最安値: 100, 最高値: 200
IComparer 実装によるカスタム比較
Min
やMax
はデフォルトの比較方法を使いますが、独自の比較ロジックを適用したい場合はIComparer<T>
を実装してカスタム比較を行うことができます。
LINQのMin
やMax
は直接IComparer
を受け取るオーバーロードはありませんが、OrderBy
やOrderByDescending
と組み合わせて最小・最大を取得する方法があります。
using System;
using System.Collections.Generic;
using System.Linq;
// 商品情報を表すクラス
class Product
{
public string Name { get; set; }
public int Price { get; set; }
}
// Product を価格の昇順で比較するコンパレータ
class PriceComparer : IComparer<Product>
{
public int Compare(Product x, Product y)
{
// 価格の昇順で比較
return x.Price.CompareTo(y.Price);
}
}
// エントリポイントを持つプログラム本体
class Program
{
static void Main()
{
// 商品リストを作成
var products = new List<Product>
{
new Product { Name = "A", Price = 100 },
new Product { Name = "B", Price = 200 },
new Product { Name = "C", Price = 150 }
};
// PriceComparer を用意
var comparer = new PriceComparer();
// 昇順ソートして最初を取得 → 最安値
var minProduct = products.OrderBy(p => p, comparer).First();
// 降順ソートして最初を取得 → 最高値
var maxProduct = products.OrderByDescending(p => p, comparer).First();
// 結果を出力
Console.WriteLine($"最安値の商品: {minProduct.Name} ({minProduct.Price})");
Console.WriteLine($"最高値の商品: {maxProduct.Name} ({maxProduct.Price})");
}
}
最安値の商品: A (100)
最高値の商品: B (200)
この方法では、OrderBy
にカスタム比較子を渡してソートし、最初の要素を取得することで最小値や最大値を得ています。
IComparer
を使うことで、価格以外の基準や複雑な比較ロジックも柔軟に実装可能です。
Aggregate による高度な集約
単純な累積合算を実装する流れ
Aggregate
メソッドはLINQの中でも特に柔軟な集約処理を実現できるメソッドです。
シーケンスの要素を1つずつ処理し、累積的に結果を生成します。
単純な合計を求める場合もAggregate
で実装可能ですが、Sum
よりも処理の流れを明示的に制御できます。
以下は整数配列の合計をAggregate
で求める例です。
int[] numbers = { 1, 2, 3, 4, 5 };
int sum = numbers.Aggregate((acc, current) => acc + current);
Console.WriteLine($"合計: {sum}");
合計: 15
ここでのAggregate
は、最初の要素を初期値としてacc
に設定し、2番目の要素から順にacc + current
の計算を繰り返します。
結果として全要素の合計が得られます。
初期値を指定しない場合、空のシーケンスに対しては例外が発生するため注意が必要です。
ValueTuple を用いて複数結果を同時取得
Aggregate
の強みは、単一の値だけでなく複数の値を同時に集約できる点にあります。
C# 7.0以降で使えるValueTuple
を活用すると、最小値・最大値・合計・件数など複数の統計情報を1回の走査で効率的に取得できます。
以下はValueTuple
を使った例です。
int[] numbers = { 3, 7, 2, 9, 4 };
var result = numbers.Aggregate(
(min: int.MaxValue, max: int.MinValue, sum: 0, count: 0),
(acc, current) => (
min: current < acc.min ? current : acc.min,
max: current > acc.max ? current : acc.max,
sum: acc.sum + current,
count: acc.count + 1
),
acc => (
acc.min,
acc.max,
acc.sum,
acc.count,
average: acc.count == 0 ? (double?)null : (double)acc.sum / acc.count
)
);
Console.WriteLine($"最小値: {result.min}");
Console.WriteLine($"最大値: {result.max}");
Console.WriteLine($"合計: {result.sum}");
Console.WriteLine($"件数: {result.count}");
Console.WriteLine($"平均: {result.average}");
最小値: 2
最大値: 9
合計: 25
件数: 5
平均: 5
このコードでは、初期値としてmin
に最大値、max
に最小値、sum
とcount
を0に設定しています。
累積関数で各値を更新し、最後に平均を計算して返しています。
1回の走査で複数の統計値を効率的に取得できるため、大量データの処理に適しています。
初期値設定と例外対策のポイント
Aggregate
を使う際は初期値の設定が重要です。
初期値を指定しない場合、空のシーケンスに対して呼び出すとInvalidOperationException
が発生します。
これを防ぐために、初期値を明示的に指定する方法が推奨されます。
int[] empty = { };
int sum = empty.Aggregate(0, (acc, current) => acc + current);
Console.WriteLine($"空配列の合計: {sum}");
空配列の合計: 0
この例では初期値0
を指定しているため、空の配列でも例外が発生せず、合計は0となります。
また、複雑な集約処理では累積関数内で例外が発生する可能性もあるため、適切なエラーハンドリングを行うことが望ましいです。
例えば、null
値の扱いや型変換の失敗などに注意してください。
さらに、Aggregate
の第3引数に変換関数を指定することで、最終結果の形を自由に変換できます。
これにより、集約中の内部状態と最終的な返却値を分けて管理できます。
int[] numbers = { 3, 7, 2, 9, 4 };
var result = numbers.Aggregate(
(sum: 0, count: 0),
(acc, current) => (acc.sum + current, acc.count + 1),
acc => acc.count == 0 ? 0 : acc.sum / acc.count
);
Console.WriteLine($"平均値: {result}");
平均値: 5
この例では、集約中は合計と件数を保持し、最後に平均値を計算して返しています。
初期値の設定と変換関数の活用で、柔軟かつ安全な集約処理が可能です。
GroupBy を使ったカテゴリ別集計
単一キーでカテゴリ合算
GroupBy
メソッドは、シーケンスの要素を指定したキーでグループ化し、各グループごとに集計処理を行う際に使います。
単一のキーでグループ化し、カテゴリ別の合計や件数を求めるのが基本的な使い方です。
以下は商品のカテゴリごとに価格の合計を求める例です。
using System;
using System.Collections.Generic;
using System.Linq;
namespace SampleApp
{
public record Item
{
public string Category { get; init; }
public int Price { get; init; }
}
class Program
{
static void Main()
{
// サンプルデータの作成
var items = new List<Item>
{
new Item { Category = "Food", Price = 10 },
new Item { Category = "Food", Price = 15 },
new Item { Category = "Electronics", Price = 100 },
new Item { Category = "Electronics", Price = 200 }
};
// カテゴリごとにグループ化し、合計価格を計算
var grouped = items.GroupBy(item => item.Category);
foreach (var group in grouped)
{
int totalPrice = group.Sum(item => item.Price);
Console.WriteLine($"カテゴリ: {group.Key}, 合計価格: {totalPrice}");
}
}
}
}
カテゴリ: Food, 合計価格: 25
カテゴリ: Electronics, 合計価格: 300
この例では、GroupBy
でCategory
をキーにグループ化し、各グループのPrice
をSum
で合計しています。
group.Key
でグループのキー(カテゴリ名)を取得できます。
匿名型 Select で結果を整形
集計結果をそのまま表示するだけでなく、匿名型を使って結果を整形し、必要な情報だけをまとめて取得することが多いです。
Select
メソッドを使って、グループのキーと集計値を匿名型で返す例を示します。
var summary = items
.GroupBy(item => item.Category)
.Select(group => new
{
Category = group.Key,
TotalPrice = group.Sum(item => item.Price),
Count = group.Count()
})
.ToList();
foreach (var s in summary)
{
Console.WriteLine($"カテゴリ: {s.Category}, 合計価格: {s.TotalPrice}, 件数: {s.Count}");
}
カテゴリ: Food, 合計価格: 25, 件数: 2
カテゴリ: Electronics, 合計価格: 300, 件数: 2
このように、匿名型で集計結果をまとめると、後続の処理や表示がシンプルになります。
複合キーによる多次元集計
複数のプロパティを組み合わせてグループ化したい場合は、匿名型やタプルをキーとして使います。
これにより、複数の条件で多次元的に集計が可能です。
以下は、商品のカテゴリと価格帯(100未満、100以上)でグループ化し、合計価格を求める例です。
var groupedMulti = items
.GroupBy(item => new
{
item.Category,
PriceRange = item.Price < 100 ? "Low" : "High"
})
.Select(group => new
{
group.Key.Category,
group.Key.PriceRange,
TotalPrice = group.Sum(item => item.Price),
Count = group.Count()
})
.ToList();
foreach (var g in groupedMulti)
{
Console.WriteLine($"カテゴリ: {g.Category}, 価格帯: {g.PriceRange}, 合計価格: {g.TotalPrice}, 件数: {g.Count}");
}
カテゴリ: Food, 価格帯: Low, 合計価格: 25, 件数: 2
カテゴリ: Electronics, 価格帯: High, 合計価格: 300, 件数: 2
このように複合キーを使うことで、より詳細な集計が可能になります。
拡張メソッドで可読性を高める書き方
複合キーのグループ化は匿名型の記述が長くなりがちなので、拡張メソッドを作成して可読性を向上させる方法があります。
例えば、価格帯を判定するロジックを拡張メソッドに切り出します。
public static class ItemExtensions
{
public static string GetPriceRange(this Item item)
{
return item.Price < 100 ? "Low" : "High";
}
}
これを使ってグループ化を簡潔に書けます。
var groupedExt = items
.GroupBy(item => new { item.Category, PriceRange = item.GetPriceRange() })
.Select(group => new
{
group.Key.Category,
group.Key.PriceRange,
TotalPrice = group.Sum(item => item.Price),
Count = group.Count()
})
.ToList();
foreach (var g in groupedExt)
{
Console.WriteLine($"カテゴリ: {g.Category}, 価格帯: {g.PriceRange}, 合計価格: {g.TotalPrice}, 件数: {g.Count}");
}
カテゴリ: Food, 価格帯: Low, 合計価格: 25, 件数: 2
カテゴリ: Electronics, 価格帯: High, 合計価格: 300, 件数: 2
拡張メソッドを使うことで、グループ化のキー定義がシンプルになり、コードの可読性と保守性が向上します。
Lookup と ToLookup の選択基準
Lookup
はIEnumerable<IGrouping<TKey, TElement>>
に似たデータ構造で、キーごとに要素を効率的に検索できるコレクションです。
ToLookup
メソッドはシーケンスからLookup
を生成します。
GroupBy
は遅延実行で、クエリが実行されるまでグループ化処理は行われませんToLookup
は即時実行で、呼び出し時にすべての要素をグループ化してLookup
を作成します
選択基準
特徴 | GroupBy | ToLookup |
---|---|---|
実行タイミング | 遅延実行 | 即時実行 |
返却型 | IEnumerable<IGrouping<TKey, TElement>> | ILookup<TKey, TElement> |
再利用性 | クエリを再実行するたびに処理される | 一度作成すれば何度でも高速にアクセス可能 |
用途 | 一度だけ集計して結果を使う場合 | 複数回キー検索や高速アクセスが必要な場合 |
例えば、複数回同じキーで要素を検索する場合はToLookup
で事前にグループ化しておくと効率的です。
var lookup = items.ToLookup(item => item.Category);
foreach (var item in lookup["Food"])
{
Console.WriteLine($"Foodカテゴリの商品価格: {item.Price}");
}
Foodカテゴリの商品価格: 10
Foodカテゴリの商品価格: 15
一方、単純に1回だけグループ化して集計したい場合はGroupBy
で十分です。
このように、処理の目的やパフォーマンス要件に応じてGroupBy
とToLookup
を使い分けることが重要です。
条件付き集計とフィルタリング
Where と集計の組み合わせ最適化
LINQで条件付きの集計を行う際、Where
メソッドと集計メソッドを組み合わせることが一般的です。
Where
で条件に合う要素を絞り込み、その後にCount
やSum
、Average
などの集計を行います。
int[] numbers = { 1, 2, 3, 4, 5, 6 };
// 3以上の数の合計を求める
int sum = numbers.Where(n => n >= 3).Sum();
Console.WriteLine($"3以上の数の合計: {sum}");
3以上の数の合計: 18
この方法は直感的でわかりやすいですが、パフォーマンス面で注意が必要です。
Where
で絞り込んだ結果に対して集計が行われるため、シーケンスを2回走査しているように見えることがあります。
しかし、LINQの遅延実行により、実際は1回の走査で条件判定と集計が同時に行われるため、過剰なパフォーマンス低下は通常ありません。
ただし、複雑な条件や大規模データの場合は、条件を集計メソッドのラムダ式に直接渡す方法も検討できます。
int sumDirect = numbers.Sum(n => n >= 3 ? n : 0);
Console.WriteLine($"3以上の数の合計(ラムダ内条件): {sumDirect}");
3以上の数の合計(ラムダ内条件): 18
この方法は1回の走査で済みますが、条件に合わない要素を0として加算するため、意図しない結果になる可能性がある点に注意してください。
条件に合わない要素を除外したい場合はWhere
との組み合わせが安全です。
Null 値を含むデータへの対処
LINQで集計を行う際、シーケンスにnull
値が含まれていると例外が発生したり、意図しない結果になることがあります。
特にオブジェクトのプロパティを集計する場合は、null
チェックを適切に行う必要があります。
var items = new[]
{
new { Name = "A", Price = (int?)100 },
new { Name = "B", Price = (int?)null }, // キャスト必須
new { Name = "C", Price = (int?)200 }
};
// nullを除外して合計を計算
int totalPrice = items.Where(item => item.Price.HasValue).Sum(item => item.Price.Value);
Console.WriteLine($"合計価格(null除外): {totalPrice}");
合計価格(null除外): 300
Price
がnull
の要素をWhere
で除外し、Sum
で合計を計算しています。
null
を含むままSum
を呼ぶと例外が発生するため、必ずnull
チェックを行いましょう。
また、null
を0として扱いたい場合は、null
合体演算子を使ってデフォルト値を設定できます。
int totalPriceWithDefault = items.Sum(item => item.Price ?? 0);
Console.WriteLine($"合計価格(nullは0として扱う): {totalPriceWithDefault}");
合計価格(nullは0として扱う): 300
この方法はコードが簡潔になり、null
を含むデータでも安全に集計できます。
DefaultIfEmpty で空集合に備える
LINQの集計メソッドは空のシーケンスに対して呼び出すと例外を投げることがあります。
例えば、Average
やMin
、Max
は空集合に対して呼び出すとInvalidOperationException
が発生します。
これを防ぐためにDefaultIfEmpty
メソッドを使って空集合にデフォルト値を設定する方法があります。
int[] empty = { };
// 空集合に0を設定して平均を計算
double average = empty.DefaultIfEmpty(0).Average();
Console.WriteLine($"空集合の平均(デフォルト0): {average}");
空集合の平均(デフォルト0): 0
DefaultIfEmpty
はシーケンスが空の場合に指定したデフォルト値を1つだけ含むシーケンスに置き換えます。
これにより、集計メソッドが例外を投げることなく安全に処理できます。
同様にMin
やMax
でも使えます。
int min = empty.DefaultIfEmpty(int.MaxValue).Min();
int max = empty.DefaultIfEmpty(int.MinValue).Max();
Console.WriteLine($"空集合の最小値(デフォルトMaxValue): {min}");
Console.WriteLine($"空集合の最大値(デフォルトMinValue): {max}");
空集合の最小値(デフォルトMaxValue): 2147483647
空集合の最大値(デフォルトMinValue): -2147483648
ただし、デフォルト値の選択は集計の意味に合うように慎重に行う必要があります。
例えば、平均値のデフォルトを0にするのは自然ですが、最小値のデフォルトをint.MaxValue
にするのは特殊なケースです。
このように、DefaultIfEmpty
を活用して空集合に備えることで、例外を防ぎつつ安全に集計処理を行えます。
集計結果の再利用と変換
SelectMany で平坦化しながら集計を適用
SelectMany
は、ネストされたコレクションを1つの平坦なシーケンスに変換するためのLINQメソッドです。
複数の子コレクションを持つオブジェクトの集計や、階層構造のデータを扱う際に非常に便利です。
平坦化した後に集計メソッドを適用することで、複雑なデータ構造から効率的に集計結果を得られます。
以下は、複数の注文を持つ顧客リストから、すべての注文の合計金額を求める例です。
class Order
{
public int Id { get; set; }
public decimal Amount { get; set; }
}
class Customer
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
}
var customers = new List<Customer>
{
new Customer
{
Name = "Alice",
Orders = new List<Order>
{
new Order { Id = 1, Amount = 100m },
new Order { Id = 2, Amount = 150m }
}
},
new Customer
{
Name = "Bob",
Orders = new List<Order>
{
new Order { Id = 3, Amount = 200m }
}
}
};
// 全顧客のすべての注文の合計金額を計算
decimal totalAmount = customers
.SelectMany(c => c.Orders)
.Sum(o => o.Amount);
Console.WriteLine($"全注文の合計金額: {totalAmount}");
全注文の合計金額: 450
この例では、SelectMany
で顧客ごとの注文リストを平坦化し、すべての注文を1つのシーケンスにまとめています。
その後、Sum
で合計金額を計算しています。
SelectMany
を使うことで、ネストされたコレクションの集計がシンプルに書けます。
また、SelectMany
は複数のネストレベルがある場合にも有効です。
例えば、注文に複数の商品が含まれている場合、さらに深い階層を平坦化して集計できます。
Let 句で中間結果を保持するテクニック
LINQのクエリ式(クエリ構文)では、let
句を使って中間結果を変数として保持できます。
これにより、同じ計算を繰り返すことなく、集計や変換の効率化とコードの可読性向上が図れます。
以下は、商品の価格に対して割引率を適用し、割引後の価格を計算した上で、割引後価格の合計を求める例です。
var products = new[]
{
new { Name = "ProductA", Price = 100m, DiscountRate = 0.1m },
new { Name = "ProductB", Price = 200m, DiscountRate = 0.2m },
new { Name = "ProductC", Price = 300m, DiscountRate = 0.15m }
};
var query = from p in products
let discountedPrice = p.Price * (1 - p.DiscountRate)
select new
{
p.Name,
discountedPrice
};
decimal totalDiscountedPrice = query.Sum(x => x.discountedPrice);
foreach (var item in query)
{
Console.WriteLine($"{item.Name} の割引後価格: {item.discountedPrice}");
}
Console.WriteLine($"割引後価格の合計: {totalDiscountedPrice}");
ProductA の割引後価格: 90
ProductB の割引後価格: 160
ProductC の割引後価格: 255
割引後価格の合計: 505
let
句で割引後価格を一度計算して変数discountedPrice
に保持しているため、select
句や集計処理で再利用できます。
これにより、同じ計算を複数回書く必要がなくなり、コードがすっきりします。
let
句は複雑な計算や条件分岐の結果を一時的に保存したい場合に特に有効です。
メソッドチェーン(メソッド構文)で同様の処理を行う場合は、一時変数を使うか、Select
で中間結果を生成してから集計に渡す形になります。
var discountedProducts = products
.Select(p => new
{
p.Name,
DiscountedPrice = p.Price * (1 - p.DiscountRate)
});
decimal totalDiscounted = discountedProducts.Sum(p => p.DiscountedPrice);
foreach (var item in discountedProducts)
{
Console.WriteLine($"{item.Name} の割引後価格: {item.DiscountedPrice}");
}
Console.WriteLine($"割引後価格の合計: {totalDiscounted}");
ProductA の割引後価格: 90
ProductB の割引後価格: 160
ProductC の割引後価格: 255
割引後価格の合計: 505
このように、let
句やSelect
で中間結果を保持することで、集計結果の再利用や変換が効率的に行えます。
パフォーマンス最適化
逐次処理と Parallel LINQ の比較
LINQの集計処理は基本的に逐次処理(シングルスレッド)で実行されますが、大量データを扱う場合はParallel LINQ
(PLINQ)を使って並列処理を行うことでパフォーマンスを向上させることが可能です。
PLINQはAsParallel()
メソッドを使って簡単に導入でき、複数のCPUコアを活用して処理を分散します。
以下は、逐次処理とPLINQでの合計計算の比較例です。
int[] largeNumbers = Enumerable.Range(1, 100_000_000).ToArray();
// 逐次処理
var sw = System.Diagnostics.Stopwatch.StartNew();
long sumSequential = largeNumbers.Sum(n => (long)n);
sw.Stop();
Console.WriteLine($"逐次処理の合計: {sumSequential}, 時間: {sw.ElapsedMilliseconds} ms");
// 並列処理
sw.Restart();
long sumParallel = largeNumbers.AsParallel().Sum(n => (long)n);
sw.Stop();
Console.WriteLine($"並列処理の合計: {sumParallel}, 時間: {sw.ElapsedMilliseconds} ms");
逐次処理の合計: 5000000050000000, 時間: 221 ms
並列処理の合計: 5000000050000000, 時間: 71 ms
この例では、PLINQを使うことで約2倍以上の高速化が見られました。
並列処理はスレッドの切り替えや同期コストが発生するため、データ量が少ない場合や処理が軽い場合は逆に遅くなることもあります。
また、PLINQは副作用のある処理や順序が重要な処理には向いていません。
集計のような副作用のない純粋な関数型処理に適しています。
メモリと CPU を抑える実装例
パフォーマンス最適化では、CPU時間だけでなくメモリ使用量も重要です。
LINQのクエリは遅延実行が基本ですが、ToList()
やToArray()
で即時実行するとメモリに大量のデータを展開するため注意が必要です。
例えば、大量データの集計でメモリを抑えるには、必要なデータだけを絞り込んでから集計を行うことが基本です。
var filteredSum = largeNumbers
.Where(n => n % 2 == 0) // 偶数だけに絞る
.Sum(n => (long)n);
Console.WriteLine($"偶数の合計: {filteredSum}");
また、Aggregate
を使って1回の走査で複数の集計を同時に行うと、複数回の列挙を避けられ、CPU負荷を軽減できます。
var stats = largeNumbers.Aggregate(
(min: int.MaxValue, max: int.MinValue, sum: 0L, count: 0),
(acc, val) => (
min: val < acc.min ? val : acc.min,
max: val > acc.max ? val : acc.max,
sum: acc.sum + val,
count: acc.count + 1
)
);
Console.WriteLine($"最小値: {stats.min}, 最大値: {stats.max}, 合計: {stats.sum}, 件数: {stats.count}");
このように、複数の集計を1回の列挙で済ませることで、CPUとメモリの両方を効率的に使えます。
ORM 経由での LINQ 集計に潜む落とし穴
Entity FrameworkやLINQ to SQLなどのORMを使う場合、LINQの集計メソッドはSQLクエリに変換されてデータベース側で実行されます。
しかし、ORM特有の制約や変換の問題により、意図しないパフォーマンス低下や例外が発生することがあります。
過剰なデータ取得
例えば、ToList()
でデータを一旦メモリに読み込んでから集計を行うと、データベースの集計機能を使わずに大量のデータを転送してしまい、パフォーマンスが著しく低下します。
// NG: 全件取得してからメモリ上で集計
var allItems = dbContext.Items.ToList();
int totalPrice = allItems.Sum(i => i.Price);
これを避けるために、集計はできるだけクエリの中で行い、SQLに変換させるべきです。
// OK: SQLで集計を実行
int totalPrice = dbContext.Items.Sum(i => i.Price);
サポートされないメソッドや式
ORMはすべての.NETメソッドや式をSQLに変換できるわけではありません。
Aggregate
のような複雑な集計やカスタム関数は変換できず、例外が発生することがあります。
その場合は、クエリを分割してメモリ上で処理するか、SQLビューやストアドプロシージャを使うなどの対策が必要です。
遅延実行と接続管理
ORMのLINQクエリは遅延実行されるため、集計メソッドを呼ぶタイミングでデータベース接続が開かれます。
複数回クエリを実行すると接続のオーバーヘッドが増えるため、必要な集計はまとめて行うことが望ましいです。
// 複数回クエリを実行する例(非推奨)
int count = dbContext.Items.Count();
int sum = dbContext.Items.Sum(i => i.Price);
これを1回のクエリで済ませる方法として、匿名型でまとめて取得する方法があります。
var stats = dbContext.Items
.GroupBy(i => 1)
.Select(g => new
{
Count = g.Count(),
Sum = g.Sum(i => i.Price)
})
.FirstOrDefault();
このように、ORM経由のLINQ集計ではSQL変換の特性や接続管理に注意し、効率的なクエリ設計を心がけることが重要です。
デバッグと検証
ToList・ToArray のタイミングで状態を確認
LINQクエリは遅延実行が基本であり、実際の処理は結果が必要になったタイミングで初めて実行されます。
そのため、デバッグ時にクエリの状態や結果を確認したい場合は、ToList()
やToArray()
を使って即時実行させることが有効です。
これにより、クエリの評価が強制され、結果の内容を簡単に検証できます。
int[] numbers = { 1, 2, 3, 4, 5, 6 };
var evenNumbersQuery = numbers.Where(n => n % 2 == 0);
// まだ実行されていない状態
Console.WriteLine("クエリ定義後");
// 即時実行して結果を取得
var evenNumbersList = evenNumbersQuery.ToList();
Console.WriteLine("偶数のリスト:");
foreach (var num in evenNumbersList)
{
Console.WriteLine(num);
}
クエリ定義後
偶数のリスト:
2
4
6
ToList()
やToArray()
を呼ぶことで、クエリが評価され、結果がメモリ上に展開されます。
これにより、デバッガーのウォッチウィンドウやログ出力で中間結果を確認しやすくなります。
ただし、大量データの場合はToList()
やToArray()
で全件をメモリに読み込むため、メモリ消費に注意が必要です。
必要な範囲だけを絞り込んでから即時実行することが望ましいです。
ログ出力とウォッチ式による結果チェック
LINQクエリの動作を検証する際、ログ出力やデバッガーのウォッチ式を活用すると効果的です。
特に複雑な集計や条件付きクエリでは、途中の状態を確認することでバグの早期発見や理解が深まります。
ログ出力の例
クエリの途中でSelect
を使い、要素の状態をログに出力しながら処理を進める方法です。
var numbers = new[] { 1, 2, 3, 4, 5, 6 };
var query = numbers
.Where(n => n % 2 == 0)
.Select(n =>
{
Console.WriteLine($"処理中の要素: {n}");
return n;
})
.ToList();
Console.WriteLine("処理完了");
処理中の要素: 2
処理中の要素: 4
処理中の要素: 6
処理完了
このように、Select
内でログを出すことで、どの要素が処理されているかをリアルタイムで把握できます。
デバッガーのウォッチ式活用
Visual StudioなどのIDEでは、ウォッチ式にLINQクエリや変数を登録して、実行時の値を確認できます。
遅延実行のクエリはウォッチ式で評価されるため、クエリの状態をリアルタイムに観察可能です。
ただし、ウォッチ式でクエリを評価すると副作用が起きる場合や、パフォーマンスに影響が出ることがあるため注意してください。
デバッグ用の中間変数を使う
複雑なクエリは段階的に中間変数に代入し、それぞれの状態をログやウォッチで確認すると理解しやすくなります。
var filtered = numbers.Where(n => n % 2 == 0);
var projected = filtered.Select(n => n * 10);
var resultList = projected.ToList();
foreach (var val in resultList)
{
Console.WriteLine(val);
}
このように分割することで、どの段階で期待と異なる結果が出ているかを特定しやすくなります。
これらの方法を組み合わせて使うことで、LINQの集計処理の動作を正確に把握し、効率的にデバッグや検証が行えます。
まとめ
この記事では、C#のLINQを使った集計処理の基本から応用まで幅広く解説しました。
Count
やSum
、GroupBy
、Aggregate
などの代表的なメソッドの使い方や、複雑な条件付き集計、複数結果の同時取得、パフォーマンス最適化のポイントも紹介しています。
さらに、デバッグや検証のテクニックも取り上げ、実践的なコード例を通じて効率的かつ安全にLINQ集計を活用する方法が理解できます。