MongoDB中find()函數返回一個遊標,用戶端通過對遊標進行一些設定就能對查詢結果進行有效地控制,如可以限制查詢得到的結果數量、跳過部分結果、或對結果集按任意鍵進行排序等。我們之前在Shell中進行操作,都是直接使用find()函數,並沒有使用其傳回值,如:
> for(var i=0; i<100; i++){... db.coll.insert({"x" : i});... }> db.coll.find();{ "_id" : ObjectId("5023997e1ed370450fbdcf89"), "x" : 25 }{ "_id" : ObjectId("5023997e1ed370450fbdcf8a"), "x" : 26 }{ "_id" : ObjectId("5023997e1ed370450fbdcf8b"), "x" : 27 }{ "_id" : ObjectId("5023997e1ed370450fbdcf8c"), "x" : 28 }{ "_id" : ObjectId("5023997e1ed370450fbdcf8d"), "x" : 29 }{ "_id" : ObjectId("5023997e1ed370450fbdcf8e"), "x" : 30 }{ "_id" : ObjectId("5023997e1ed370450fbdcf8f"), "x" : 31 }{ "_id" : ObjectId("5023997e1ed370450fbdcf90"), "x" : 32 }{ "_id" : ObjectId("5023997e1ed370450fbdcf91"), "x" : 33 }{ "_id" : ObjectId("5023997e1ed370450fbdcf92"), "x" : 34 }{ "_id" : ObjectId("5023997e1ed370450fbdcf93"), "x" : 35 }{ "_id" : ObjectId("5023997e1ed370450fbdcf94"), "x" : 36 }{ "_id" : ObjectId("5023997e1ed370450fbdcf95"), "x" : 37 }{ "_id" : ObjectId("5023997e1ed370450fbdcf96"), "x" : 38 }{ "_id" : ObjectId("5023997e1ed370450fbdcf97"), "x" : 39 }{ "_id" : ObjectId("5023997e1ed370450fbdcf98"), "x" : 40 }{ "_id" : ObjectId("5023997e1ed370450fbdcf99"), "x" : 41 }{ "_id" : ObjectId("5023997e1ed370450fbdcf9a"), "x" : 42 }{ "_id" : ObjectId("5023997e1ed370450fbdcf9b"), "x" : 43 }{ "_id" : ObjectId("5023997e1ed370450fbdcf9c"), "x" : 44 }has more>
我們先通過javascript指令碼向集合中填充100條文檔,然後直接調用find函數。其會自動遞迴find返回的遊標,將前20條資料展示在shell中。如果我們通過變數保留find函數的傳回值,其不會自動進行遍曆顯示操作:
> var cursor = db.coll.find();>
這樣做,實際發生的是,調用完find後,此時Shell並不會去真正地訪問資料庫,而是等待開始要求獲得結果的時候才向資料庫發送查詢請求。我們此時可以對這個遊標進行各種設定,然後調用遊標的hashNext()或next()方法,這樣就會真正訪問資料庫,這是一個懶載入的過程。如下:
> var cursor = db.coll.find();> while(cursor.hasNext()){... var doc = cursor.next();... // do stuff with doc... };>
上述代碼中,當調用cursor.hasNext()時,查詢被發往資料庫,預設會返回前100條文檔或者前4M的資料(兩者之中較小的),這樣下次next或hasNext都是本地調用了。當這組資料被遍曆完畢,hasNext會導致再次去訪問資料庫,直到所有結果被返回。
【遊標的操作】
上面提到了,當獲得遊標後,我們可以先對遊標進行處理後,再讓訪問資料庫的動作按照我們的意願發生。這裡有3個函數可以在處理遊標時使用:limit、skip、sort。limit是限制遊標返回的數量,指定了上限;skip是忽略前面的部分文檔,如果文檔總數量小於忽略的數量,則返回空集合;sort對得到的子集合進行排序,可以按照多個鍵進行正反排序。對遊標的操作有一個技巧就是,操作遊標的函數返回的都是遊標,所以可以組成方法鏈調用,如下:
> db.fruitprice.find();{ "_id" : ObjectId("50226b4c3becfacce6a22a5b"), "apple" : 10, "banana" : 6, "pear" : 3 }{ "_id" : ObjectId("50226ba63becfacce6a22a5c"), "apple" : 10, "watermelon" : 3, "pear" : 3 }{ "_id" : ObjectId("5023a1db7dceac1a6dacb0b7"), "apple" : 8, "orange" : 4, "tomato" : 3 }{ "_id" : ObjectId("5023a1eb7dceac1a6dacb0b8"), "apple" : 9, "orange" : 5, "grape" : 12 }{ "_id" : ObjectId("5023a2037dceac1a6dacb0b9"), "melon" : 7, "orange" : 3, "grape" : 11 }> db.fruitprice.find().sort({"apple":1, "banana":-1});{ "_id" : ObjectId("5023a2037dceac1a6dacb0b9"), "melon" : 7, "orange" : 3, "grape" : 11 }{ "_id" : ObjectId("5023a1db7dceac1a6dacb0b7"), "apple" : 8, "orange" : 4, "tomato" : 3 }{ "_id" : ObjectId("5023a1eb7dceac1a6dacb0b8"), "apple" : 9, "orange" : 5, "grape" : 12 }{ "_id" : ObjectId("50226b4c3becfacce6a22a5b"), "apple" : 10, "banana" : 6, "pear" : 3 }{ "_id" : ObjectId("50226ba63becfacce6a22a5c"), "apple" : 10, "watermelon" : 3, "pear" : 3 }> db.fruitprice.find().skip(1).limit(3).sort({"apple":1, "banana":-1});{ "_id" : ObjectId("5023a1db7dceac1a6dacb0b7"), "apple" : 8, "orange" : 4, "tomato" : 3 }{ "_id" : ObjectId("5023a1eb7dceac1a6dacb0b8"), "apple" : 9, "orange" : 5, "grape" : 12 }{ "_id" : ObjectId("50226b4c3becfacce6a22a5b"), "apple" : 10, "banana" : 6, "pear" : 3 }>
上述,共執行了3次查詢:
第一次只執行了find函數,返回了一個集合,沒有順序。
第二次,我們讓其按照鍵"apple" 鍵"banana"排序,鍵"apple"升序(>0的數字),鍵“banana”降序(<0的數字),即先按找鍵“apple”升序排,對於鍵“apple”相等的文檔,則按照鍵“banana”降序排。我們看到,按照這種方式排,沒有鍵“apple”(即鍵“apple”值為null)的文檔排在了第一位,這是在MongoDB中,針對相同鍵不同類型值有一個預設順序,我們後面會提到。
第三次,我們使用了三個函數來設定遊標。這三個函數的關係是,在資料庫伺服器端,先執行sort,然後再排好序的文檔上執行skip,最後按照limit設定的最大數量返迴文檔子集即可。
【相同鍵不同類型值的比較順序】
按照鍵排序時,MongoDB中對於鍵並不會強制其值是什麼類型,我們在實際中也會遇到同一個鍵,一個文檔中為串在另一個文檔中為數字,這種文檔在排序時是任何進行的呢。MongoDB中,有一個預先定義的順序,從小到大,依次為:
(1):最小值
(2):null
(3):數字(整型,長整型,雙精確度)
(4):字串
(5):對象/文檔
(6):數組
(7):位元據
(8):對象ID
(9):布爾值
(10):日期型
(11):時間戳記
(12):Regex
(13):最大值
在上述第二種查詢中,我們按鍵“apple”查,在一個文檔中缺少這個鍵,即在這個文檔中這個鍵的值為null,在其他文檔中該鍵的值都是數字,按照上述順序,缺少這個鍵的文檔按升序排理應排在前面。
【避免使用skip略過大量結果】
使用skip略過少量文檔效率不會有什麼影響,如果略過大量結果,則可能會產生效能瓶頸。對於skip,我們通常的應用可能是在分頁時。對於分頁,我們有兩種方式來應對:
1. 將分頁的處理放在應用程式層,即將資料全部查出,然後在應用程式層處理分頁顯示。這就是通常所說的偽分頁。
2. 如果分頁必須在資料庫端進行,這通常是資料量太大的情況。這時,我們先嘗試使用skip操作,如果出現效能瓶頸,我們只能根據一個排序鍵,在擷取下頁資料時,首先根據上一頁最後一個文檔中該鍵的值來查詢文檔,最後排序截取即可。這樣就可以避免使用skip。
【進階查詢選項】
查詢分為普通查詢和封裝查詢,我們上面示範的各種查詢方式都是普通查詢, 如下我們再示範一個:
> db.fruitprice.find({"apple":10}).sort({"banana":1});{ "_id" : ObjectId("50226ba63becfacce6a22a5c"), "apple" : 10, "watermelon" : 3, "pear" : 3 }{ "_id" : ObjectId("50226b4c3becfacce6a22a5b"), "apple" : 10, "banana" : 6, "pear" : 3 }>
上述查詢我們轉換為封裝形式的寫法是:
> db.fruitprice.find({"$query" : {"apple" : 10}, "$orderby" : {"banana" : 1}});{ "_id" : ObjectId("50226ba63becfacce6a22a5c"), "apple" : 10, "watermelon" : 3, "pear" : 3 }{ "_id" : ObjectId("50226b4c3becfacce6a22a5b"), "apple" : 10, "banana" : 6, "pear" : 3 }>
我們的所有查詢在發送到資料庫端時,都被提前轉換成了封裝形式。封裝形式就是額外使用了一些鍵,如上述的"$query","$orderby"。我們還有如下一些有用的鍵可用:
1. $maxscan : integer 指定查詢時最多掃描文檔的數量
2. $min : document 查詢的開始條件
3. $max : document 查詢的結束條件
4. $hint : document 指定伺服器使用哪些索引進行查詢
5. $explain : boolean 擷取查詢細節,如用到的索引,結果數量,耗時等,類似於關聯式資料庫這邊查看執行計畫。並不會真正執行查詢
6. $snapshot : boolean 確保查詢的結果是在查詢執行那一刻的一致快照。這個在後面還會提到。
【擷取一致結果】
我們從MongoDB中擷取到資料後,通常會執行這種操作:對文檔進行處理後,即時地更新到資料庫中。這時對於大量文檔的情況有可能產生一個問題,我來描述一下:前面提到了,我們從資料庫端調用遊標的hasNext時,資料庫預設會返回100條文檔給我們,我們開始操作。假設對於一個文檔,我們增大了其大小,並且超高了MongoDB為文檔設定的預留地區,這時我們將這條文檔更新到資料庫中,資料庫沒法將其放置在其原始位置上,只能將其移動,通常會移動到集合末尾。這樣我們再次擷取文檔時,有可能又得到這條以被修改的文檔。。
應對這個問題,我們的方法就是對查詢結果進行快照。如果使用了上面提到的“$snapshop”選項,查詢就是針對不變的集合視圖啟動並執行。這點我們只是描述一下,實際情況中可以不用擔心了,因為MongoDB中,所有返回一組的查詢實際都進行了快照。