離線資料集 (DataSet/DataTable) 需要像 SQL 一樣做 Inner Join,不想/不能回資料庫。ADO.NET 提供 DataRelation 機制可高效完成。
using System.Data;
public static class DataTableExtensions
{
///
/// 執行 Inner Join,回傳合併欄位的新 DataTable
///
public static DataTable InnerJoin(
this DataTable left,
DataTable right,
IEnumerable leftKeys,
IEnumerable rightKeys)
{
if (left == null || right == null) throw new ArgumentNullException();
if (leftKeys.Count() != rightKeys.Count()) throw new ArgumentException("鍵欄位數量必須相同");
var result = new DataTable("JoinResult");
using (var ds = new DataSet())
{
// 1. 複製結構與資料到 DataSet (避免污染原表)
ds.Tables.Add(left.Copy());
ds.Tables.Add(right.Copy());
// 2. 取得複製後的對應欄位
var leftKeyCols = leftKeys.Select(c => ds.Tables[0].Columns[c.ColumnName]).ToArray();
var rightKeyCols = rightKeys.Select(c => ds.Tables[1].Columns[c.ColumnName]).ToArray();
// 3. 建立關聯
var relation = new DataRelation("JoinRelation", leftKeyCols, rightKeyCols, false);
ds.Relations.Add(relation);
// 4. 建立結果表結構 (處理同名欄位)
foreach (DataColumn c in ds.Tables[0].Columns)
result.Columns.Add(c.ColumnName, c.DataType);
foreach (DataColumn c in ds.Tables[1].Columns)
{
string name = result.Columns.Contains(c.ColumnName)
? c.ColumnName + "_Right" // 衝突改名
: c.ColumnName;
result.Columns.Add(name, c.DataType);
}
// 5. 依關聯填入資料
result.BeginLoadData();
foreach (DataRow leftRow in ds.Tables[0].Rows)
{
DataRow[] rightRows = leftRow.GetChildRows(relation);
if (rightRows.Length == 0) continue; // Inner Join:無對應則略過
object[] leftValues = leftRow.ItemArray;
foreach (DataRow rightRow in rightRows)
{
object[] rightValues = rightRow.ItemArray;
var joined = new object[leftValues.Length + rightValues.Length];
Array.Copy(leftValues, 0, joined, 0, leftValues.Length);
Array.Copy(rightValues, 0, joined, leftValues.Length, rightValues.Length);
result.LoadDataRow(joined, true);
}
}
result.EndLoadData();
}
return result;
}
/// 單欄位鍵簡化版
public static DataTable InnerJoin(this DataTable left, DataTable right, string leftKey, string rightKey)
=> left.InnerJoin(right, new[] { left.Columns[leftKey] }, new[] { right.Columns[rightKey] });
}
// 模擬資料
var orders = new DataTable("Orders");
orders.Columns.Add("OrderId", typeof(int));
orders.Columns.Add("CustomerId", typeof(int));
orders.Columns.Add("Amount", typeof(decimal));
orders.Rows.Add(1, 101, 1500);
orders.Rows.Add(2, 102, 2300);
orders.Rows.Add(3, 103, 900);
var customers = new DataTable("Customers");
customers.Columns.Add("CustomerId", typeof(int));
customers.Columns.Add("Name", typeof(string));
customers.Columns.Add("City", typeof(string));
customers.Rows.Add(101, "Alice", "Taipei");
customers.Rows.Add(102, "Bob", "Taichung");
customers.Rows.Add(104, "Cindy", "Kaohsiung"); // 無訂單
// Join
var joined = orders.InnerJoin(customers, "CustomerId", "CustomerId");
foreach (DataRow row in joined.Rows)
{
Console.WriteLine($"OrderId={row["OrderId"]}, Customer={row["Name"]}, City={row["City"]}, Amount={row["Amount"]}");
}
/*
OrderId=1, Customer=Alice, City=Taipei, Amount=1500
OrderId=2, Customer=Bob, City=Taichung, Amount=2300
*/
| 方案 | 優點 | 缺點 | 適用資料量 |
|---|---|---|---|
| DataRelation + GetChildRows | API 內建、無額外相依 | 建立關聯有開銷、記憶體佔用大 (複製表) | < 5 萬列 |
| LINQ to DataTable | 語法簡潔、可讀性高 | 逐列比對 O(N×M)、極慢 | < 1 萬列 |
| Dictionary Lookup (手寫) | O(N+M) 線性、記憶體效率佳 | 需自行撰寫 | **任意大小,推薦** |
| 回資料庫 Join | 資料庫最佳化、索引支援 | 需網路、連線 | 大資料量首選 |
public static DataTable FastInnerJoin(DataTable left, DataTable right, string key)
{
var result = left.Clone();
foreach (DataColumn c in right.Columns)
if (!result.Columns.Contains(c.ColumnName)) result.Columns.Add(c.ColumnName, c.DataType);
else result.Columns.Add(c.ColumnName + "_Right", c.DataType);
// 建立右表索引
var rightIndex = new Dictionary
此法避免 DataSet 複製開銷,單次掃描建立雜湊索引,大量資料 (百萬列) 亦能秒級完成。