LINQ (Language Integrated Query)
LINQとは
LINQ(Language Integrated Query)は、C#のコード内でSQLのようなクエリ構文を使用して、コレクション、データベース、XMLなどの様々なデータソースを一貫した方法で操作できる機能です。
メリット
- 統一された構文: 配列、リスト、DBなど、データソースが異なっても同じ書き方で操作できる。
- 型安全性: コンパイル時に型チェックが行われるため、実行時エラーを減らせる。
- IntelliSense: 強力な入力補完が効くため、開発効率が高い。
基本的な構文
LINQには「クエリ構文」と「メソッド構文」の2種類があります。現在はメソッド構文(ラムダ式)が主流です。
メソッド構文(推奨)
var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
// 偶数のみを取得し、2倍にする
var result = numbers
.Where(n => n % 2 == 0) // フィルタリング
.Select(n => n * 2) // 射影(変換)
.ToList(); // リストに変換(ここで実行される)
クエリ構文
var result = from n in numbers
where n % 2 == 0
select n * 2;
遅延実行 (Deferred Execution)
LINQの最も重要な概念の一つが「遅延実行」です。
Where や Select などのクエリ定義メソッドを呼び出した時点では、クエリは実行されません。
foreach で列挙するか、ToList() や Count() などの即時実行メソッドを呼び出したタイミングで初めて実行されます。
var numbers = new List<int> { 1, 2, 3 };
// クエリを定義(まだ実行されない)
var query = numbers.Select(n => n * 10);
numbers.Add(4); // データソースを変更
// ここで実行される(4も含まれる!)
foreach (var n in query)
{
Console.WriteLine(n); // 10, 20, 30, 40
}
この特性を理解していないと、意図しないタイミングでDBアクセスが発生したり、データ変更が反映されたりするバグの原因になります。
IEnumerable<T> vs IQueryable<T>
LINQ to Objects(メモリ内のコレクション操作)と LINQ to Entities(EF CoreなどのDB操作)では、使用されるインターフェースが異なります。
| インターフェース | 名前空間 | 主な用途 | フィルタリング場所 |
|---|---|---|---|
IEnumerable<T> | System.Collections.Generic | メモリ内コレクション (List, Array) | クライアント側(メモリ上) |
IQueryable<T> | System.Linq | 外部データソース (SQL DB) | サーバー側(DB上) |
注意点
EF Coreを使用している場合、不用意に IEnumerable<T> にキャストしたり、途中で ToList() を呼んだりすると、全件データをメモリに取得してからフィルタリングすることになり、深刻なパフォーマンス問題を引き起こします。
❌ Bad Code (全件取得後にフィルタ)
// ToList() した時点で SQL が発行され、全件メモリにロードされる
var users = _context.Users.ToList();
// メモリ上でフィルタリング(遅い、メモリ消費大)
var activeUsers = users.Where(u => u.IsActive).ToList();
✅ Good Code (SQLでフィルタ)
// まだ SQL は発行されない (IQueryable のまま)
var query = _context.Users.Where(u => u.IsActive);
// ここで "WHERE IsActive = 1" を含む SQL が発行される
var activeUsers = query.ToList();
よく使うメソッド
| メソッド | 説明 | SQL相当 |
|---|---|---|
Where | 条件に一致する要素を抽出 | WHERE |
Select | 要素を変換・射影する | SELECT |
OrderBy / OrderByDescending | ソート | ORDER BY |
GroupBy | グループ化 | GROUP BY |
Join | 結合 | INNER JOIN |
First / FirstOrDefault | 最初の要素を取得 | TOP 1 |
Any | 条件を満たす要素が存在するか確認 | EXISTS |
Count | 要素数を取得 | COUNT |
ベストプラクティス
1. 複数回の列挙を避ける (Avoid Multiple Enumeration)
IEnumerable をそのまま何度も foreach や Count() で使用すると、その都度クエリが再実行されます。
データソースがデータベースや重い計算処理の場合、パフォーマンスに悪影響を与えます。
複数回使用する場合は、ToList() や ToArray() でメモリ上に実体化(Materialize)します。
❌ Bad Code
// query は IEnumerable<T>
if (query.Count() > 0) // ここで1回実行
{
foreach (var item in query) // ここでもう1回実行
{
// ...
}
}
✅ Good Code
var list = query.ToList(); // ここで1回だけ実行してメモリに保持
if (list.Count > 0) // メモリ上のリストのプロパティアクセス(高速)
{
foreach (var item in list) // メモリ上のリストを列挙
{
// ...
}
}
2. 射影 (Projection) を活用する
必要なカラムだけを Select で取得することで、メモリ使用量と転送量を削減できます。
特にEF Coreでは、エンティティ全体を取得するよりも、必要なプロパティだけの匿名クラスやDTOに射影する方が高速です。
// 必要なデータだけを取得
var userNames = _context.Users
.Where(u => u.IsActive)
.Select(u => new { u.Id, u.Name }) // 匿名クラスへの射影
.ToList();
3. 複合クエリの可読性
LINQはメソッドチェーンで長く書くことができますが、複雑になりすぎると可読性が下がります。 意味のある単位で変数に分割することで、コードが読みやすくなり、デバッグも容易になります。
var activeUsers = _context.Users.Where(u => u.IsActive);
var highValueUsers = activeUsers.Where(u => u.TotalPurchase > 10000);
var result = highValueUsers.Select(u => u.Name).ToList();