iT邦幫忙

DAY 27
2

分享一些學習心得系列 第 27

LINQ自學筆記-語法應用-聚合資料-Join-1

Join 運算子,是 LINQ 標準查詢運算子中,應用難度較高的運算子,我盡量用範例引導的方式讓大家熟悉它。
自學筆記這系列是我自己學習的一些心得分享,歡迎指教。這系列的分享,會以 C# + 我比較熟的 Net 3.5 環境為主。
另外本系列預計至少會切成【打地基】和【語法應用】兩大部分做分享。打地基的部分,講的是 LINQ 的組成元素,這部分幾乎和 LINQ 無關,反而是 C# 2.0、C# 3.0 的一堆語言特性,例如:型別推斷、擴充方法、泛型、委派等等,不過都會把分享的範圍限制在和 LINQ 應用有直接相關功能。
PS. LINQ 自學筆記幾乎所有範例,都可直接複製到 LINQPad 4 上執行(大多是用 Statements 和 Program 模式)。因為它輕巧好用,功能強大,寫範例很方便,請大家自行到以下網址下載最新的 LINQPad:http://www.LINQpad.net/。
Join 運算子,可以讓我們把兩個來源序列聚合為一個輸出序列。Join 運算子有兩個多載方法,但是第二個多載方法 LINQ to Entities 和 LINQ to SQL 並不支援,使用時請注意:

public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>( 
    this IEnumerable<TOuter> outer, 
    IEnumerable<TInner> inner, 
    Func<TOuter, TKey> outerKeySelector, 
    Func<TInner, TKey> innerKeySelector, 
    Func<TOuter, TInner, TResult> resultSelector 
)
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>( 
    this IEnumerable<TOuter> outer, 
    IEnumerable<TInner> inner, 
    Func<TOuter, TKey> outerKeySelector, 
    Func<TInner, TKey> innerKeySelector, 
    Func<TOuter, TInner, TResult> resultSelector, 
    IEqualityComparer<TKey> comparer 
)

Join 運算子的邏輯,大致上是這樣:

  1. 依序將 inner 所有項目(TInner)取出來,傳給 innerKeySelecor 產生 TKey,然後存到一個 Hashtable 中。
  2. 依序將 outer 所有項目(TOuter)取出來,傳給 outerKeySelector 產生 TKey,到步驟 1 產生的 Hashtable 查詢是否有相符的項目。不符合就重覆步驟 2 (取 outer 中下一個 TOuter),符合就進行步驟 3。
  3. 符合的話,就把 TOuter 和相符的 TInner 當做參數,傳給 resultSelector,取回 TResult,指定到準備輸出的 IEnumerable<TResult> 中。

Join 運算子有幾個要注意的地方:

  1. Join 是擴充 IEnumerable<TOuter>,但是回傳是 IEnumerable<TResult>,表示輸出序列型別和輸入不同。
  2. outer、inner、outerKeySelector、innerKeySelector 或 resultSelector 為 null,執行時會發生 ArgumentNullException 例外。
  3. 兩個序列 Join 時,是利用 KeySelector 所取出的 TKey 做相等比較,第一個多載方法用 TKey 型別預設比較子,第二個多載方法則使用自訂的相等比較子。
  4. Join 可用在查詢表達式和方法架構查詢中,用查詢表達式撰寫比較輕鬆,建議使用。
  5. 兩個序列 Join 時,會以 outer 中的項目索引順序加入輸出序列中,例如:outer 有 B、A、C,inner 是 A、B、C,最後 Join 結果是三個項目都要輸出,則預設是以 outer 的 B、A、C 順序輸出。
  6. Join 運算子是以相等聯結(Equijoin)的方式聚合兩個聚列,也就是說,Join 後的輸出序列中的項目,一定是因為該項目透過 outerKeySelector 和 innerKeySelector 取出之 TKey 值相等所致。

在看範例程式碼前,我們要先建立兩個資料來源序列,以利後續 Demo,為了方便大家使用,所以採用 LINQ to Objects 的方式處理,做資料來源的程式碼如下:

void Main() 
{ 
    var Customers = DataProvider.getCustomers(); 
    var Orders = DataProvider.getOrders(); 
    Customers.Dump(); 
    Orders.Dump(); 
}

public static class DataProvider 
{ 
    public static List<Customer> getCustomers() 
    { 
        var Customers = new List<Customer> 
        { 
            new Customer {ID = 1, Name = "Leo"}, 
            new Customer {ID = 2, Name = "Rose"}, 
            new Customer {ID = 3, Name = "Alvin"}, 
            new Customer {ID = 4, Name = "Emy"}, 
            new Customer {ID = 5, Name = "Alice"}, 
            new Customer {ID = 6, Name = "Bobo"} 
        }; 
        return Customers; 
    } 
    public static List<Order> getOrders() 
    { 
        var Orders = new List<Order> 
        { 
            new Order {ID = 1, CustomerID = 1, Date = new DateTime(2012,1,5), Description = "Mouse", Price = 480}, 
            new Order {ID = 2, CustomerID = 1, Date = new DateTime(2012,2,15), Description = "Books", Price = 880}, 
            new Order {ID = 3, CustomerID = 2, Date = new DateTime(2011,6,16), Description = "Keyboard", Price = 290}, 
            new Order {ID = 4, CustomerID = 2, Date = new DateTime(2012,3,25), Description = "NoteBook", Price = 16800}, 
            new Order {ID = 5, CustomerID = 3, Date = new DateTime(2012,8,15), Description = "Mouse", Price = 480}, 
            new Order {ID = 6, CustomerID = 4, Date = new DateTime(2011,6,22), Description = "NoteBook", Price = 16800}, 
            new Order {ID = 7, CustomerID = 4, Date = new DateTime(2011,10,10), Description = "Mouse", Price = 480}, 
            new Order {ID = 8, CustomerID = 4, Date = new DateTime(2012,9,8), Description = "Camera", Price = 29900}, 
        }; 
        return Orders; 
    } 
}

public class Customer 
{ 
    public int ID { get; set; } 
    public string Name { get; set; } 
} 
public class Order 
{ 
    public int ID { get; set; } 
    public int CustomerID { get; set; } 
    public DateTime Date { get; set; } 
    public string Description { get; set; } 
    public Decimal Price { get; set; } 
} 


準備好資料來源後,我們來看最簡單的 Join 範例:

//請注意:建立資料來源的程式碼就不重覆貼了,請自行參閱前述內容
void Main() 
{ 
    var Customers = DataProvider.getCustomers(); 
    var Orders = DataProvider.getOrders();

    var query = from c in Customers 
                join o in Orders on c.ID equals o.CustomerID 
                select c.Name + " 買了 " + o.Description; 
    query.Dump(); 
//    Customers.Join ( 
//        Orders, 
//        c => c.ID, 
//        o => o.CustomerID, 
//        (c, o) => ((c.Name + " 買了 ") + o.Description) 
//    ).Dump(); 
}


Join 運算子,在查詢表達式中,必須搭配 in、on 和 equals 三個關鍵字使用。上述範例註解的部分是對等的方法架構查詢語法。範例程式碼說明如下:

  1. 我們將 Customers 序列為主體(outer),將 Orders 序列加入聯結(inner)。
  2. 設定相等聯結的主鍵(TKey)是 Customer(TOuter) 的 ID 和 Order(TInner) 的 CustomerID。
  3. TKey 相符的話,則把 Customer.Name 和 Order.Description 組成一個字串後,放到輸出序列 IEnumerable<String> 中。

這個範例請務必了解,因為這是最簡單的版本,也是基礎樣式,若不能融會貫通,後續的範例就更難理解了。

接著我們要研究一個小題目,就是 Join 運算子和之前學過的 SelectMany 運算子,大多時候可以互相替代,以上述範例來看,可改寫成以下 SelectMany 運算子的樣式:

void Main() 
{ 
    var Customers = DataProvider.getCustomers(); 
    var Orders = DataProvider.getOrders();

//    var query = from c in Customers 
//                join o in Orders on c.ID equals o.CustomerID 
//                select c.Name + " 買了 " + o.Description; 
    var query = 
        from c in Customers
        from o in Orders where c.ID == o.CustomerID 
        select c.Name + " 買了 " + o.Description; 
    query.Dump(); 
//    Customers.SelectMany( 
//                c => Orders, 
//                (c, o) => new {c = c, o = o}) 
//             .Where (temp0 => (temp0.c.ID == temp0.o.CustomerID)) 
//             .Select (temp0 => ((temp0.c.Name + " 買了 ") + temp0.o.Description)) 
//             .Dump(); 
}

上述沒有註解的查詢表達式,用了兩個 from,並透過第二個 from 的 where 關鍵子指定相等聯結的欄位,此語法轉譯為方法架構查詢,就是後面註解起來的區塊。即然可以替代,那誰比較好呢?這問題沒有正確答案,看個人喜好就行,不過至少可以研究看看,那一種方式效能會比較好呢?

就我個人實務應用的經驗,都是 Join 比較快,當然我們要有實驗精神,所以我把上述的範例延伸為 Join v.s. SelectMany 的效能比賽,範例程式如下:

void Main() 
{ 
    var Customers = DataProvider.getCustomers(); 
    var Orders = DataProvider.getOrders(); 
    Console.WriteLine("Customer 數量:" + Customers.Count()); 
    Console.WriteLine("Orders 數量:" + Orders.Count()); 
    Stopwatch sw = new Stopwatch(); 
    sw.Start(); 
    var slowQuery = 
        from c in Customers 
        from o in Orders where c.ID == o.CustomerID 
        select c.Name + " 買了 " + o.Description; 
    Console.WriteLine("Slow query result count: " + slowQuery.Count()); 
    sw.Stop(); 
    Console.WriteLine("Slow query with SelectMany (Milliseconds): " + (sw.ElapsedMilliseconds)); 
    sw.Restart(); 
    var fastQuery = 
        from c in Customers 
        join o in Orders on c.ID equals o.CustomerID 
        select c.Name + " 買了 " + o.Description; 
    Console.WriteLine("Fast query result count: " + fastQuery.Count()); 
    sw.Stop(); 
    Console.WriteLine("Fast query with Join (Milliseconds): " + (sw.ElapsedMilliseconds)); 
}
/* 輸出:
Customer 數量:10000 
Orders 數量:12000 
Slow query result count: 10008 
Slow query with SelectMany (Milliseconds): 16199 
Fast query result count: 10008 
Fast query with Join (Milliseconds): 9
*/

為了效能測試,我微調了產生 Customers、Orders 的程式,一次可產出如上圖 10,000 和 12,000 筆資料,且有 10,008 筆資料可以聯結起來(故意不做完全對應),最後輸出兩種運算子的查詢結果筆數及所花費的時間,證明相同的查詢結果,Join 運算子在大量資料查詢時,效能遠勝 SelectMany 運算子。

其實這個結果要說明差異點很簡單,從兩種運算子的方法架構查詢語法即可一窺究竟:

Customers.SelectMany (c => Orders, (c, o) => new {c = c, o = o}) 
         .Where (temp0 => (temp0.c.ID == temp0.o.CustomerID)) 
         .Select (temp0 => ((temp0.c.Name + " 買了 ") + temp0.o.Description)) 




 
Customers.Join ( 
      Orders, 
      c => c.ID, 
      o => o.CustomerID, 
      (c, o) => ((c.Name + " 買了 ") + o.Description) 
   ) 

最重要的差異就是,SelectMany 運算子一開始要先把 Customers 和 Orders 中所有資料全部展開,產生新的 IEnumerable<匿名型別> 序列,然後才進行 Where 條件過濾,最後才整理出要回傳的 TResult。也就是說,為了取回我們要的結果,它必須產生三個 IEnumerable 序列才能得到結果,記憶體的消耗極大,當然就快不起來。

Join 運算子不一樣,如同文章開頭的敘述,它一開始先列舉 inner 中所有項目,將主鍵存到 Hashtable 中,然後逐一列舉 outer 中的項目,拿 outer 的主鍵到 Hashtable 中對應,有符合就整理成要輸出的匿名型別資料,並指定到 IEnumerable<TResult> 中。兩種運算子執行作業的方式大不相同,當然也就造成效能表現上的不同。


上一篇
LINQ自學筆記-語法應用-設定方法-Range、Repeat、Empty、Distinct
下一篇
LINQ自學筆記-語法應用-聚合資料-Join-2
系列文
分享一些學習心得30

尚未有邦友留言

立即登入留言