2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】LINQにおける結合処理をイメージで理解する

Posted at

はじめに

C#でコレクション型のオブジェクトに対して操作を行う際に、言語機能の一つであるLINQを使うことが多いと思います。

Program.cs
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

以降では、内部結合左外部結合について、クエリ演算子の使い方や、シーケンスの状態をイメージ図で見ていきます。

サンプルデータ

Program.cs
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()

実装例

Program.cs
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" }
  }
]

処理イメージ
この結果から、以下イメージの通りデータを取得したことが分かります。
image.png

内部結合ということで、意図した通り、categoriesのId:C003は取得結果に含まれていません。

Join() ⇒ 内部結合(INNER JOIN)を実現

左外部結合

左外部結合をLINQで実現するためのクエリ演算子は、GroupJoin() + SelectMany()ですが、処理イメージが分かりやすいため順を追って見ていきます。

GroupBy()

はじめに、GroupBy()を確認します。
その名の通り、グループ化(SQLにおけるGROUP BY)を実現するメソッドです。

実装例

Program.cs
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" }
  ]
]

処理イメージ
image.png

グループ化キーをもとに、オブジェクトを配列化していることが分かります。

GroupJoin()

続いて、GroupJoin()を確認します。
こちらは、GroupBy() + Join()に近い動きをします。

実装例

Program.cs
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": []
  }
]

処理イメージ
image.png

はじめに結合先シーケンスのグループ化を行い、その後それらを結合元シーケンスに結合します。

結合処理に関して、単純なJoin()との違いですが、GroupJoin()では左外部結合を行うため、結合元シーケンスのレコードは全て取得します。
(categoriesのId:C003が取得結果に含まれていることが分かります。また、結合先シーケンスは空配列となります)

GroupJoin() + SelectMany()

先ほどまでで、だいぶ左外部結合らしくなってきました。
ただ、配列化したオブジェクトを保持する都合上、階層が一段深くなり、少し扱いづらいです。

そこで、多階層のコレクションをフラット化するSelectMany()を使用します。

実装例

Program.cs
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
  }
]

処理イメージ
image.png

配列化したオブジェクトがなくなり、階層がすっきりしました。

SelectMany()の第1引数では、DefaultIfEmpty()を指定することで、存在しない結合先シーケンスをnullにするよう設定しています。

このように、GroupJoin()とSelectMany()を併用することで、SQLにおけるLEFT JOINを実現していることが分かります。

GroupJoin() + SelectMany() ⇒ 左外部結合(LEFT JOIN)を実現

動作環境

  • Windows 11
  • C# 12.0
  • .NET 8.0
  • Visual Studio 2022
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?