C#MongoDB 分頁查詢的方法及效能

來源:互聯網
上載者:User

標籤:

 

傳統的SQL分頁

傳統的sql分頁,所有的方案幾乎是繞不開row_number的,對於需要各種排序,複雜查詢的情境,row_number就是殺手鐧。另外,針對現在的web很流行的poll/push載入分頁的方式,一般會利用時間戳記來實現分頁。 這兩種分頁可以說前者是通用的,連Linq產生的分頁都是row_number,可想而知它多通用。後者是無論是效能和複雜程度都是最好的,因為只要簡單的一個時間戳記即可。

MongoDB分頁

進入到Mongo的思路,分頁其實並不難,那難得是什嗎?其實倒也沒啥,看明白了也就那樣,和SQL分頁的思路是一致的。

先說明下這篇文章使用的用例,我在資料庫裡匯入了如下的實體資料,其中cus_id、amount我產生為有序的數字,倒入的記錄數是200w:

public class Test{        /// <summary>        /// 主鍵 ObjectId 是MongoDB內建的主鍵類型        /// </summary>        public ObjectId Id { get; set; }        /// <summary>        /// 客戶編碼        /// </summary>        [BsonElement("cust_id")]        public string CustomerId { get; set; }        /// <summary>        /// 總數        /// </summary>        [BsonElement("amount")]        public int Amount { get; set; }        /// <summary>        /// 狀態        /// </summary>        [BsonElement("status")]        public string Status { get; set; }}
以下的操作基於MongoDB GUI 工具見參考資料3

首先來看看分頁需要的參數以及結果,一般的分頁需要的參數是:

  • PageIndex    當前頁
  • PageSize      每頁記錄數
  • QueryParam[]  其他的查詢欄位

所以按照row_number的分頁思想,也就是說取第(pageIndex*pageSize)到第(pageIndex*pageSize + pageSize),我們用Linq表達就是:

query.Where(xxx...xxx).Skip(pageIndex*pageSize).Take(pageSize)

尋找了資料,還真有skip函數,而且還有Limit函數 見參考資料1、2,於是輕易地實現了這樣的分頁查詢:

db.test.find({xxx...xxx}).sort({"amount":1}).skip(10).limit(10)//這裡忽略掉查詢語句

 相當的高效,幾乎是幾毫秒就出來了結果,果然是NoSql效率一流。但是慢,我這裡使用的資料只是10條而已,並沒有很多資料。我把資料加到100000,效率大概是20ms。如果這麼簡單就研究結束了的話,那真的是太辜負了程式猿要鑽研的精神了。sql分頁的方案,方案可是能有一大把,效率也是不一的,那Mongo難道就這一種,答案顯然不是這樣的。另外是否效率上,效能上會有問題呢?Redis篇裡,就吃過這樣的虧,亂用Keys。

在查看了一些資料之後,發現所有的資料都是這樣說的:

不要輕易使用Skip來做查詢,否則資料量大了就會導致效能急劇下降,這是因為Skip是一條一條的數過來的,多了自然就慢了。

 這麼說Skip就要避免使用了,那麼如何避免呢?首先來回顧SQL分頁的後一種時間戳記分頁方案,這種利用欄位的有序性質,利用查詢來取資料的方式,可以直接避免掉了大量的數數。也就是說,如果能附帶上這樣的條件那查詢效率就會提高,事實上是這樣的嗎?我們來驗證一下:

這裡我們假設查詢第100001條資料,這條資料的Amount值是:2399927,我們來寫兩條語句分別如下:

db.test.sort({"amount":1}).skip(100000).limit(10)  //183msdb.test.find({amount:{$gt:2399927}}).sort({"amount":1}).limit(10)  //53ms

 

結果已經附帶到注釋了,很明顯後者的效能是前者的三分之一,差距是非常大的。也印證了Skip效率差的理論。

C#實現

上面已經談過了MongoDB分頁的語句和效率,那麼我們來實現C#驅動版本。

本篇文章裡使用的是官方的BSON驅動,詳見參考資料4。Mongo驅動附帶了另種方式一種是類似ADO.NET的原生query,一種是Linq,這裡我們兩種都實現

方案一:條件查詢 原生Query實現

var query = Query<Test>.GT(item => item.Amount, 2399927);                var result = collection.Find(query).SetLimit(100)                       .SetSortOrder(SortBy.Ascending("amount")).ToList();              Console.WriteLine(result.First().ToJson());//BSON內建的ToJson

方案二:Skip原生Query實現

var result = collection.FindAll().SetSkip(100000).SetLimit(100)             .SetSortOrder(SortBy.Ascending("amount"));Console.WriteLine(result.ToList().First().ToJson());

方案三:Linq 條件查詢

var result = collection.AsQueryable<Test>().OrderBy(item => item.Amount)         .Where(item => item.Amount > 2399927).Take(100);Console.WriteLine(result.First().ToJson());

方案四:Linq Skip版本

 var result = collection.AsQueryable<Test>().OrderBy(item => item.Amount).Skip(100000).Take(100);Console.WriteLine(result.First().ToJson());

效能比較參考

這裡的測試代碼稍後我上傳一下,具體的實現是利用了老趙(我的偶像啊~)的CodeTimer來計算效能。另外我跑代碼都是用TestDriven外掛程式來跑的。
方案一:pagination GT-Limit{ "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" }Time Elapsed:    1,322msCPU Cycles:    4,442,427,252Gen 0:         0Gen 1:         0Gen 2:         0

 

方案二:pagination Skip-limit{ "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" }Time Elapsed:    95msCPU Cycles:    18,280,728Gen 0:         0Gen 1:         0Gen 2:         0
方案三:paginatiLinq on Linq where{ "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" }

Time Elapsed: 76ms
CPU Cycles: 268,734,988
Gen 0: 0
Gen 1: 0
Gen 2: 0

方案四:pagination Linq Skip{ "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" }Time Elapsed:    97msCPU Cycles:    30,834,648Gen 0:         0Gen 1:         0Gen 2:         0

上面結果是不是大跌眼鏡,這和理論實在相差太大,第一次為什麼和後面的差距如此大?剛開始我以為是C# Mongo的驅動問題,嘗試了換驅動也差不多。這幾天我在看《MongoDB in Action》的時候,發現文章裡提到:

MongoDB會根據查詢,來載入文檔的索引和中繼資料到記憶體裡,並且建議文檔中繼資料的大小始終要保持小於機器記憶體,否則效能會下降。

 注意到了上面的理論之後,我替換了我的測試方案,第一次執行排除下,然後再比較,發現確實結果正常了。

方案一的修正結果:

pagination GT-Limit{ "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" }Time Elapsed:   18msCPU Cycles:     54,753,796Gen 0:          0Gen 1:          0Gen 2:          0

 

總結

這篇文章,基於Skip分頁和有序欄位查詢分頁兩種方案進行的對比。後者說白了只是利用查詢結果不用依次數數來提高了效能。Skip雖然效率低一些但是通用一些,有序欄位的查詢,需要在設計分頁的時候對這個欄位做一些處理,起碼要點了頁碼能擷取到這個欄位。這裡我附加一個方式,就是兩者的結合,我們可以拿每次展示的那頁資料上的最後一個,結合Skip來處理分頁,這樣的話,相對來說更好一些。這裡就不具體實現了。其他方式的效能比較和實現,歡迎大牛們來分享,十分感謝。另外本篇中如有紕漏和不足請留言指教。

  忘記打個小廣告,我們公司招人哦,詳情見我部落格的副標題!!

參考資料

1. MongoDB Skip函數:http://docs.mongodb.org/manual/reference/operator/aggregation/skip/

2. MongoDB Limit函數:http://docs.mongodb.org/manual/reference/operator/aggregation/limit/

3. MongoVUE Windows用戶端管理工具(有收費版本):http://www.mongovue.com/ 

4. C#官方驅動:http://docs.mongodb.org/manual/applications/drivers/

C#MongoDB 分頁查詢的方法及效能

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.