はじめに
C#でコレクション型のオブジェクトに対して操作を行う際に、言語機能の一つであるLINQを使うことが多いと思います。
IEnumerable<int> numbers = Enumerable.Range(1, 10).ToList();
// --- メソッド構文 ---
var resultMethodSyntax = numbers
.Where(n => n % 2 == 0)
.OrderBy(n => n)
.Select(n => n);
// --- クエリ構文 ---
var evenNumbersQuery = from n in numbers
where n % 2 == 0
orderby n
select n;
// どちらの構文も以下のようなSQLに相当
// SELECT n
// FROM numbers
// WHERE n % 2 = 0
// ORDER BY n;
上記のように、操作対象のオブジェクト(シーケンス)に対して、SQLのような操作感を得られるのが魅力的です。
構文が2つ用意されていますが、本記事ではメソッド構文を使用します。
結合処理
シーケンスの結合処理には、以下の通り、LINQが用意しているメソッド(クエリ演算子)を使用します。
| LINQ(クエリ演算子) | SQL | |
|---|---|---|
| 内部結合 | Join() | [INNER] JOIN |
| 左外部結合 | GroupJoin() + SelectMany() | LEFT [OUTER] JOIN |
| 右外部結合 | 左右を入れ替えて左外部結合 | RIGHT [OUTER] JOIN |
| 完全外部結合 | 両外部結合結果をUnion() | FULL [OUTER] JOIN |
| 交差結合 | SelectMany() | CROSS JOIN |
以降では、内部結合・左外部結合について、クエリ演算子の使い方や、シーケンスの状態をイメージ図で見ていきます。
サンプルデータ
class Category
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
}
class Book
{
public string Title { get; set; } = string.Empty;
public string CategoryId { get; set; } = string.Empty;
}
IEnumerable<Category> categories = new List<Category>
{
new Category { Id = "C001", Name = "Fiction" },
new Category { Id = "C002", Name = "Science" },
new Category { Id = "C003", Name = "History" },
};
IEnumerable<Book> books = new List<Book>
{
new Book { Title = "Book A", CategoryId = "C001" },
new Book { Title = "Book B", CategoryId = "C001" },
new Book { Title = "Book C", CategoryId = "C002" },
};
内部結合
Join()
実装例
var result = categories
.Join(
books,
category => category.Id,
book => book.CategoryId,
(category, book) => new { category, book }
);
- 第1引数:結合先(右側テーブル)シーケンス
- 第2引数:結合元(左側テーブル)シーケンスの結合キー
- 第3引数:結合先(右側テーブル)シーケンスの結合キー
- 第4引数:結合結果として取得するオブジェクト
※メソッド呼び出し元が結合元(左側テーブル)シーケンスにあたる
※第2,3,4引数は関数を受け取るため、厳密には「~を取得するために呼び出されるデリゲート」という表現が適切ですが、簡略化します
取得結果
上記の実装例にて取得した結果を確認してみます。
(Debug.WriteLine(JsonSerializer.Serialize(result));)
[
{
"category": { "Id": "C001", "Name": "Fiction" },
"book": { "Title": "Book A", "CategoryId": "C001" }
},
{
"category": { "Id": "C001", "Name": "Fiction" },
"book": { "Title": "Book B", "CategoryId": "C001" }
},
{
"category": { "Id": "C002", "Name": "Science" },
"book": { "Title": "Book C", "CategoryId": "C002" }
}
]
処理イメージ
この結果から、以下イメージの通りデータを取得したことが分かります。

内部結合ということで、意図した通り、categoriesのId:C003は取得結果に含まれていません。
⭐Join() ⇒ 内部結合(INNER JOIN)を実現
左外部結合
左外部結合をLINQで実現するためのクエリ演算子は、GroupJoin() + SelectMany()ですが、処理イメージが分かりやすいため順を追って見ていきます。
GroupBy()
はじめに、GroupBy()を確認します。
その名の通り、グループ化(SQLにおけるGROUP BY)を実現するメソッドです。
実装例
var result = books.GroupBy(x => x.CategoryId);
// 集計関数は以下のように使用
// var list = result.Select(x => new
// {
// CategoryId = x.Key, // ←Keyプロパティでグループ化キーへアクセス
// Count = x.Count(),
// // TotalPrice = x.Sum(book => book.Price),
// });
- 引数:グループ化キー
取得結果
[
[
{ "Title": "Book A", "CategoryId": "C001" },
{ "Title": "Book B", "CategoryId": "C001" }
],
[
{ "Title": "Book C", "CategoryId": "C002" }
]
]
グループ化キーをもとに、オブジェクトを配列化していることが分かります。
GroupJoin()
続いて、GroupJoin()を確認します。
こちらは、GroupBy() + Join()に近い動きをします。
実装例
var result = categories
.GroupJoin(
books,
category => category.Id,
book => book.CategoryId,
(category, books) => new { category, books }
);
// 集計関数は以下のように使用
// var list = result.Select(x => new
// {
// CategoryName = x.category.Name,
// Count = x.books.Count(),
// // TotalPrice = x.books.Sum(book => book.Price),
// });
- 第1引数:結合先(右側テーブル)シーケンス
- 第2引数:結合元(左側テーブル)シーケンスの結合キー
- 第3引数:結合先(右側テーブル)シーケンスの結合キー、かつグループ化キー
- 第4引数:結合結果として取得するオブジェクト
取得結果
[
{
"category": { "Id": "C001", "Name": "Fiction" },
"books": [
{ "Title": "Book A", "CategoryId": "C001" },
{ "Title": "Book B", "CategoryId": "C001" }
]
},
{
"category": { "Id": "C002", "Name": "Science" },
"books": [
{ "Title": "Book C", "CategoryId": "C002" }
]
},
{
"category": { "Id": "C003", "Name": "History" },
"books": []
}
]
はじめに結合先シーケンスのグループ化を行い、その後それらを結合元シーケンスに結合します。
結合処理に関して、単純なJoin()との違いですが、GroupJoin()では左外部結合を行うため、結合元シーケンスのレコードは全て取得します。
(categoriesのId:C003が取得結果に含まれていることが分かります。また、結合先シーケンスは空配列となります)
GroupJoin() + SelectMany()
先ほどまでで、だいぶ左外部結合らしくなってきました。
ただ、配列化したオブジェクトを保持する都合上、階層が一段深くなり、少し扱いづらいです。
そこで、多階層のコレクションをフラット化するSelectMany()を使用します。
実装例
var result = categories
.GroupJoin(
books,
category => category.Id,
book => book.CategoryId,
(category, books) => new { category, books }
)
.SelectMany(
x => x.books.DefaultIfEmpty(),
(x, book) => new { x.category, book }
);
取得結果
[
{
"category": { "Id": "C001", "Name": "Fiction" },
"book": { "Title": "Book A", "CategoryId": "C001" }
},
{
"category": { "Id": "C001", "Name": "Fiction" },
"book": { "Title": "Book B", "CategoryId": "C001" }
},
{
"category": { "Id": "C002", "Name": "Science" },
"book": { "Title": "Book C", "CategoryId": "C002" }
},
{
"category": { "Id": "C003", "Name": "History" },
"book": null
}
]
配列化したオブジェクトがなくなり、階層がすっきりしました。
SelectMany()の第1引数では、DefaultIfEmpty()を指定することで、存在しない結合先シーケンスをnullにするよう設定しています。
このように、GroupJoin()とSelectMany()を併用することで、SQLにおけるLEFT JOINを実現していることが分かります。
⭐GroupJoin() + SelectMany() ⇒ 左外部結合(LEFT JOIN)を実現
動作環境
- Windows 11
- C# 12.0
- .NET 8.0
- Visual Studio 2022


