MongoDB分頁查詢的方法及效能
最近有點忙,本來有好多東西可以總結,Redis系列其實還應該有四、五、六…不過《Redis in Action》還沒讀完,等讀完再來總結,不然太水,對不起讀者。
自從上次Redis之後呢,算是對Nosql型別的產品有些入門了,這會換個方向,研究下真正的NoSql資料庫——MongoDB。說起MongoDB,確實是用完了之後顛覆了我的資料管和程式觀。怎麼說呢?如果用在OO設計的程式裡那真的太棒了,像我這種資料驅動、表驅動思想根深蒂固的人,思路很難一下子跟上MongoDB的節奏。當然並不是呼叫個api,寫幾句query那些思路,而是程式設計思路,業務領域的設計,如果OO,如何適合展現,適合查詢,適合聚合運算等等。總之MongoDB重要的是程式的設計,設計好了,其實完全就忽略了Mongo的儲存,因為mongodb實在是太方便了。
廢話不多說,關於入門的資料、安裝以及其他請拉到文章末尾,我附上了一些資料,以後如有必要再來分享。這篇文章著重的講講MongoDB的分頁查詢,為啥?分頁可是常見的頭號殺手,弄不好了,客戶罵,經理罵。
傳統的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) //183ms db.test.find({amount:{$gt:2399927}}).sort({"amount":1}).limit(10) //53ms
結果已經附帶到註釋了,很明顯後者的效能是前者的三分之一,差距是非常大的。也印證了Skip效率差的理論。
C#實現
上面已經談過了MongoDB分頁的語句和效率,那麼我們來實現C#驅動版本。
方案一:條件查詢 原生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,322ms CPU Cycles: 4,442,427,252 Gen 0: 0 Gen 1: 0 Gen 2: 0
方案二: pagination Skip-limit { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" } Time Elapsed: 95ms CPU Cycles: 18,280,728 Gen 0: 0 Gen 1: 0 Gen 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: 97ms CPU Cycles: 30,834,648 Gen 0: 0 Gen 1: 0 Gen 2: 0
上面結果是不是大跌眼鏡,這和理論實在相差太大,第一次為什麼和後面的差距如此大?剛開始我以為是C# Mongo的驅動問題,嘗試了換驅動也差不多。這幾天我在看《MongoDB in Action》的時候,發現文章裡提到:
MongoDB會根據查詢,來載入文件的索引和後設資料到記憶體裡,並且建議文件後設資料的大小始終要保持小於機器記憶體,否則效能會下降。
注意到了上面的理論之後,我替換了我的測試方案,第一次執行排除下,然後再比較,發現確實結果正常了。
方案一的修正結果:
pagination GT-Limit { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount " : 2399928, "status" : "B" } Time Elapsed: 18ms CPU Cycles: 54,753,796 Gen 0: 0 Gen 1: 0 Gen 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/
相關文章
- MySQL的分頁查詢MySql
- elasticsearch查詢之大資料集分頁效能分析Elasticsearch大資料
- Elasticsearch 分頁查詢Elasticsearch
- 分庫分表後的分頁查詢
- NET 集合分頁查詢
- AntDesignBlazor示例——分頁查詢Blazor
- ThinkPhp框架:分頁查詢PHP框架
- 分頁查詢優化優化
- SSH框架下的分頁查詢框架
- 效能優化之分頁查詢優化
- 使用SSH完成條件及分頁查詢的主要程式碼
- elasticsearch查詢之大資料集分頁查詢Elasticsearch大資料
- mysql資料庫查詢時用到的分頁方法有哪些MySql資料庫
- 直播app開發,使用koa和MongoDB實現分頁和模糊查詢APPMongoDB
- MySQL分頁查詢優化MySql優化
- indexdb實現分頁查詢Index
- MySQL——優化巢狀查詢和分頁查詢MySql優化巢狀
- Oracle總結【SQL細節、多表查詢、分組查詢、分頁】OracleSQL
- MongoDB 的分頁(Pagination)MongoDB
- 資料的儲存和查詢分離不利查詢效能 - thenewstack
- 關於 groupBy 分組查詢的分頁處理
- MongoDB - 聚合查詢MongoDB
- Oracle資料庫中的分頁查詢Oracle資料庫
- 菜品條件分頁查詢
- 【記錄】SSH分頁查詢功能
- MySQL 百萬級資料量分頁查詢方法及其最佳化MySql
- 使用Mybatis-plus進行分頁查詢,沒有分頁效果,查詢的資料量超出每頁數量設定MyBatis
- 資料庫全表查詢之-分頁查詢優化資料庫優化
- 千萬級資料深分頁查詢SQL效能最佳化實踐SQL
- 流式查詢1. mybatis的遊標Cursor,分頁大資料查詢MyBatis大資料
- OData武裝你的WEBAPI-分頁查詢WebAPI
- SSM框架實現分頁查詢例子SSM框架
- MySQL分優化之超大頁查詢MySql優化
- Hibernate5.1+Sqlserver2000分頁查詢SQLServer
- c# winform 實現分頁查詢C#ORM
- (MySQL學習筆記)分頁查詢MySql筆記
- ❖ MongoDB 高階查詢MongoDB
- mongodb慢查詢分析MongoDB
- Mongodb高階查詢MongoDB