大量資料在做Linq比對時改用Lookup/Dictionary通常可顯著提昇效能
很久沒貼文章了,趁中午空檔,來筆記一下最近效能調整一個案例的情況。下面是用 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();
dtMain 是從資料庫撈出來的資料,dtXls 則是透過 NPOI 取得使用者上傳的 Excel 資料。上述程式,目的是要把使用者上傳的資料中,同一 Id 下的 Name 必須都包含在資料庫原始資料的同一 Id 下:
但是上述的 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,光是這樣,我們看看結果:
很驚人的數據吧!快了 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));
沒時間再多寫什麼了,先這樣。
2013/4/9 修改:91 大提醒,把程式碼中 if (resultItem.Count() > 0) 改成 if (resultItem.Any()) 效能會更好,而且的確如果只是要判斷 IEnumerable<T> 中有無資料,用 Any() 更名正言順。
--------
沒什麼特別的~
不過是一些筆記而已