C# の LINQ で結合する Join
Join は2つのシーケンスをキーで結合するメソッドだ。SQL の内部結合(INNER JOIN)に相当する。
基本的な使い方
2つのコレクションを共通のキーで結合する。
var customers = new List<Customer>
{
new Customer { Id = 1, Name = "Alice" },
new Customer { Id = 2, Name = "Bob" },
new Customer { Id = 3, Name = "Charlie" }
};
var orders = new List<Order>
{
new Order { CustomerId = 1, Product = "Apple" },
new Order { CustomerId = 2, Product = "Banana" },
new Order { CustomerId = 1, Product = "Cherry" }
};
var result = customers.Join(
orders,
customer => customer.Id, // 外側のキー
order => order.CustomerId, // 内側のキー
(customer, order) => new // 結果の形
{
customer.Name,
order.Product
}
);
foreach (var item in result)
{
Console.WriteLine($"{item.Name}: {item.Product}");
}
// Alice: Apple
// Alice: Cherry
// Bob: BananaJoin の引数は4つある。
キーが一致する要素同士が組み合わされる。Alice は2つの注文を持つので、結果も2行になる。
クエリ構文での表現
クエリ構文では join ... on ... equals を使う。
var customers = new List<Customer>
{
new Customer { Id = 1, Name = "Alice" },
new Customer { Id = 2, Name = "Bob" }
};
var orders = new List<Order>
{
new Order { CustomerId = 1, Product = "Apple" },
new Order { CustomerId = 2, Product = "Banana" }
};
var result = from c in customers
join o in orders on c.Id equals o.CustomerId
select new { c.Name, o.Product };
foreach (var item in result)
{
Console.WriteLine($"{item.Name}: {item.Product}");
}on ... equals の左側が外側シーケンスのキー、右側が内側シーケンスのキーだ。順番を間違えるとエラーになる。
内部結合の性質
Join は内部結合なので、キーが一致しない要素は結果に含まれない。
var customers = new List<Customer>
{
new Customer { Id = 1, Name = "Alice" },
new Customer { Id = 2, Name = "Bob" },
new Customer { Id = 3, Name = "Charlie" } // 注文なし
};
var orders = new List<Order>
{
new Order { CustomerId = 1, Product = "Apple" },
new Order { CustomerId = 2, Product = "Banana" },
new Order { CustomerId = 99, Product = "Unknown" } // 顧客なし
};
var result = customers.Join(
orders,
c => c.Id,
o => o.CustomerId,
(c, o) => new { c.Name, o.Product }
);
foreach (var item in result)
{
Console.WriteLine($"{item.Name}: {item.Product}");
}
// Alice: Apple
// Bob: BananaCharlie は注文を持たないので結果に出ない。CustomerId = 99 の注文は対応する顧客がいないので無視される。
左外部結合(GroupJoin + SelectMany)
すべての顧客を結果に含めたい場合は、GroupJoin と SelectMany を組み合わせる。
var customers = new List<Customer>
{
new Customer { Id = 1, Name = "Alice" },
new Customer { Id = 2, Name = "Bob" },
new Customer { Id = 3, Name = "Charlie" }
};
var orders = new List<Order>
{
new Order { CustomerId = 1, Product = "Apple" },
new Order { CustomerId = 1, Product = "Cherry" }
};
var result = customers.GroupJoin(
orders,
c => c.Id,
o => o.CustomerId,
(customer, customerOrders) => new { customer, customerOrders }
)
.SelectMany(
x => x.customerOrders.DefaultIfEmpty(),
(x, order) => new
{
x.customer.Name,
Product = order?.Product ?? "(なし)"
}
);
foreach (var item in result)
{
Console.WriteLine($"{item.Name}: {item.Product}");
}
// Alice: Apple
// Alice: Cherry
// Bob: (なし)
// Charlie: (なし)DefaultIfEmpty() が空のシーケンスに対してデフォルト値(null)を返すので、注文のない顧客も結果に含まれる。
クエリ構文ではもう少し読みやすく書ける。
var result = from c in customers
join o in orders on c.Id equals o.CustomerId into customerOrders
from o in customerOrders.DefaultIfEmpty()
select new
{
c.Name,
Product = o?.Product ?? "(なし)"
};into でグループ化し、DefaultIfEmpty() で左外部結合を実現している。
複合キーでの結合
複数のプロパティをキーにする場合は、匿名型を使う。
var result = from a in tableA
join b in tableB
on new { a.Year, a.Month } equals new { b.Year, b.Month }
select new { a, b };匿名型のプロパティ名が一致していないと結合できない。両方とも Year、Month という名前でなければならない。
パフォーマンスの考慮
Join は内部的にハッシュテーブルを使うため、大量のデータでも効率的に結合できる。
ハッシュベース。O(n + m) の計算量で高速。
ネストしたループ。O(n × m) で大量データには不向き。
2つのシーケンスを結合する場合は、素朴な Where + Any よりも Join を使う方がパフォーマンスが良い。