大量資料在做Linq比對時改用請注意轉型的效能問題,且考慮使用Lookup/Dictionary以提昇效能

  • 9128
  • 0
  • 2013-04-10

大量資料在做Linq比對時改用Lookup/Dictionary通常可顯著提昇效能

dotBlogs 的標籤: , ,

很久沒貼文章了,趁中午空檔,來筆記一下最近效能調整一個案例的情況。下面是用 LINQPad 做的測試(所以會看到 Dump() 方法,它是 LINQPad 的方法,等同於 Console.WriteLine):

    DataTable dtXls = GetXlsTable();
    dtMain.Rows.Count.Dump(@"資料庫中的比對用資料筆數:");
    dtXls.Rows.Count.Dump(@"使用者要匯入的Excel資料筆數:");
    int iRowCount = 0;
    string targetId = "";
    StringBuilder sbBefore = new StringBuilder();
    StringBuilder sbAfter = new StringBuilder();
    Stopwatch sw = new Stopwatch();
    Console.WriteLine (@"===============原始效能很差的版本===============");
    sw.Start();
    for (int i = 0; i < dtXls.Rows.Count; i++)
    {
        targetId = dtXls.Rows[i].Field<string>("ID");
        var resultItem = from mItem in dtMain.AsEnumerable()
                    where mItem.Field<string>("ID") == targetId && mItem.Field<string>("STATUS") == "Y" && 
                    (from xlsItem in dtXls.AsEnumerable() 
                        where xlsItem.Field<string>("ID") == targetId
                        select xlsItem.Field<string>("NAME")).Contains(mItem.Field<string>("NAME")) == false
                    select mItem;
        if (resultItem.Any()){
            //resultItem.Dump();
            sbBefore.Append(resultItem.First().Field<string>("ID") + " - " + resultItem.First().Field<string>("NAME") + "," );
            iRowCount++;
        }
    }
    sw.Stop();
    Console.WriteLine (@"資料不符合的筆數:" + iRowCount.ToString());
    Console.WriteLine (@"耗費總秒數:" + (sw.ElapsedMilliseconds / 1000d).ToString());
    sw.Reset();

150623

dtMain 是從資料庫撈出來的資料,dtXls 則是透過 NPOI 取得使用者上傳的 Excel 資料。上述程式,目的是要把使用者上傳的資料中,同一 Id 下的 Name 必須都包含在資料庫原始資料的同一 Id 下:

CD

但是上述的 LINQ 語法有很大的效能問題!LINQ 在迴圈中,Excel 有 4188 筆,它就要跑 4188 次,而資料來源是 DataTable,所以每一次查詢前,都要先轉成 IEnumerable<DataRow>,然後取每個欄位值時,也要從 Object 轉成 String,瘋狂的轉型,對效能有地獄般的影響!所以先做一個簡單版本的修正:

    iRowCount = 0;
    sw.Start();
    var listMain = dtMain.CopyToEntityList<MainItem>();
    var listXls = dtXls.CopyToEntityList<XlsItem>();
    for (int i = 0; i < dtXls.Rows.Count ; i++)
    {
        targetId = dtXls.Rows[i].Field<string>("ID");
        var resultItem = from mItem in listMain
                        where mItem.ID == targetId && mItem.STATUS == "Y" && 
                        (from xlsItem in listXls
                            where xlsItem.ID == targetId
                            select xlsItem.NAME).Contains(mItem.NAME) == false
                        select mItem;
        if (resultItem.Any()) {
            //resultItem.Dump();
            sbAfter.Append(resultItem.First().ID + " - " + resultItem.First().NAME +  "," );
            iRowCount++;
        }
    }
    sw.Stop();
    Console.WriteLine (@"資料不符合的筆數:" + iRowCount.ToString());
    Console.WriteLine (@"耗費總秒數:" + (sw.ElapsedMilliseconds / 1000d).ToString());
    Console.WriteLine("不符合資料的原始版本和調整版本是否相同:" + sbBefore.Equals(sbAfter));
    sw.Reset();

上面透過一個 DataTable 的擴充方法:CopyToEntityList(Of T),把 DataTable 轉成 IEnumerable(Of T),這個擴充方法是我自己寫的,不過我有上傳到 http://www.extensionmethod.net/,有興趣的人請自行取用:

http://www.extensionmethod.net/vb/datatable/copytoentitylist

轉成 IEnumerable(Of T) 之後,在迴圈中就不用再 AsEnumerable(),取欄位值也不每次都從 Object -> String,光是這樣,我們看看結果:

150631

很驚人的數據吧!快了 7 倍!!!而且這個數據是包含 CopyToEntityList(Of T) 的時間(這樣比較準確)。

但是,都做效能調整了,能再快,一定要給他快下去!所以我們再想想有什麼可以調整的?

以這次的需求來說,我們要檢查使用者的 4188 筆資料,每個 ID 所擁有的 NAME,是否都和資料庫記載的一致,而資料庫有 16392 筆,雖然稱不上海量資料,但是我們要記得,IEnumerable(Of T) 和資料庫中的 Table 不同,它沒有索引值,所以沒有辦法用演算法加速查詢、比對的速度。那我們是不是可以讓他有索引值?

第一個浮現腦海的是:Dictionary<TKey, TValue> 類別,不過馬上推翻,因為 ID 和 NAME 是一對多的關係,Dictionary 的 Key 和 Value 是 1 對 1。那就換一個:Lookup<TKey, TElement> 類別,這就可以 1 對多啦~~ 微笑

來吧,最終版本:

    iRowCount = 0;
    sbAfter = new StringBuilder();
    sw.Start();
    listMain = dtMain.CopyToEntityList<MainItem>();
    listXls = dtXls.CopyToEntityList<XlsItem>();
    var lookupMain = listMain.ToLookup(mi => mi.ID + mi.STATUS);
    for (int i = 0; i < dtXls.Rows.Count ; i++)
    {
        targetId = dtXls.Rows[i].Field<string>("ID");
        var namesInId = lookupMain[targetId + "Y"];
        var resultItem = from mItem in namesInId where 
                        (from xlsItem in listXls
                            where xlsItem.ID == targetId
                            select xlsItem.NAME).Contains(mItem.NAME) == false
                        select mItem;
        if (resultItem.Any()) {
            //resultItem.Dump();
            sbAfter.Append(resultItem.First().ID + " - " + resultItem.First().NAME +  "," );
            iRowCount++;
        }
    }
    sw.Stop();
    Console.WriteLine (@"資料不符合的筆數:" + iRowCount.ToString());
    Console.WriteLine (@"耗費總秒數:" + (sw.ElapsedMilliseconds / 1000d).ToString());
    sw.Reset();
    Console.WriteLine("不符合資料的原始版本和調整版本是否相同:" + sbBefore.Equals(sbAfter));

150638

沒時間再多寫什麼了,先這樣。

2013/4/9 修改:91 大提醒,把程式碼中 if (resultItem.Count() > 0) 改成 if (resultItem.Any()) 效能會更好,而且的確如果只是要判斷 IEnumerable<T> 中有無資料,用 Any() 更名正言順。

--------
沒什麼特別的~
不過是一些筆記而已