上一篇文章: MongoDB指南---八、特定類型的查詢
下一篇文章: MongoDB指南---十、索引、複合索引 簡介
數據庫使用遊標返回find的執行結果。客戶端對遊標的實現一般可以對最終結果進行有效的控制。能夠限制結果的數量,略過部分結果,根據任意鍵按任意順序的組合對結果進行各類排序,或者是執行其餘一些強大的操做。
要想從shell中建立一個遊標,首先要對集合填充一些文檔,而後對其執行查詢,並將結果分配給一個局部變量(用var聲明的變量就是局部變量)。這裏,先建立一個簡單的集合,然後作個查詢,並用cursor變量保存結果:正則表達式
> for(i=0; i<100; i++) { ... db.collection.insert({x : i}); ... } > var cursor = db.collection.find();
這麼作的好處是能夠一次查看一條結果。若是將結果放在全局變量或者就沒有放在變量中,MongoDB shell會自動迭代,自動顯示最開始的若干文檔。也就是在這以前咱們看到的種種例子,通常你們只想經過shell看看集合裏面有什麼,而不是想在其中實際運行程序,這樣設計也就很合適。
要迭代結果,可使用遊標的next方法。也可使用hasNext來查看遊標中是否還有其餘結果。典型的結果遍歷以下所示:shell
> while (cursor.hasNext()) { ... obj = cursor.next(); ... // do stuff ... }
cursor.hasNext()檢查是否有後續結果存在,而後用cursor.next()得到它。
遊標類還實現了JavaScript的迭代器接口,因此能夠在forEach循環中使用:數據庫
> var cursor = db.people.find(); > cursor.forEach(function(x) { ... print(x.name); ... }); adam matt zak
調用find時,shell並不當即查詢數據庫,而是等待真正開始要求得到結果時才發送查詢,這樣在執行以前能夠給查詢附加額外的選項。幾乎遊標對象的每一個方法都返回遊標自己,這樣就能夠按任意順序組成方法鏈。例如,下面幾種表達是等價的:segmentfault
> var cursor = db.foo.find().sort({"x" : 1}).limit(1).skip(10); > var cursor = db.foo.find().limit(1).sort({"x" : 1}).skip(10); > var cursor = db.foo.find().skip(10).limit(1).sort({"x" : 1});
此時,查詢尚未真正執行,全部這些函數都只是構造查詢。如今,假設咱們執行以下操做:數組
> cursor.hasNext()
這時,查詢被髮往服務器。shell馬上獲取前100個結果或者前4 MB數據(二者之中較小者),這樣下次調用next或者hasNext時就沒必要再次鏈接服務器取結果了。客戶端用光了第一組結果,shell會再一次聯繫數據庫,使用getMore請求提取更多的結果。getMore請求包含一個查詢標識符,向數據庫詢問是否還有更多的結果,若是有,則返回下一批結果。這個過程會一直持續到遊標耗盡或者結果所有返回。服務器
最經常使用的查詢選項就是限制返回結果的數量、忽略必定數量的結果以及排序。全部這些選項必定要在查詢被髮送到服務器以前指定。
要限制結果數量,可在find後使用limit函數。例如,只返回3個結果,能夠這樣:app
> db.c.find().limit(3)
要是匹配的結果不到3個,則返回匹配數量的結果。limit指定的是上限,而非下限。
skip與limit相似:dom
> db.c.find().skip(3)
上面的操做會略過前三個匹配的文檔,而後返回餘下的文檔。若是集合裏面能匹配的文檔少於3個,則不會返回任何文檔。
sort接受一個對象做爲參數,這個對象是一組鍵/值對,鍵對應文檔的鍵名,值表明排序的方向。排序方向能夠是1(升序)或者-1(降序)。若是指定了多個鍵,則按照這些鍵被指定的順序逐個排序。例如,要按照"username"升序及"age"降序排序,能夠這樣寫:函數
> db.c.find().sort({username : 1, age : -1})
這3個方法能夠組合使用。這對於分頁很是有用。例如,你有個在線商店,有人想搜索mp3。如果想每頁返回50個結果,並且按照價格從高到低排序,能夠這樣寫:性能
> db.stock.find({"desc" : "mp3"}).limit(50).sort({"price" : -1})
點擊「下一頁」能夠看到更多的結果,經過skip也能夠很是簡單地實現,只須要略過前50個結果就行了(已經在第一頁顯示了):
> db.stock.find({"desc" : "mp3"}).limit(50).skip(50).sort({"price" : -1})
然而,略過過多的結果會致使性能問題,下一小節會講述如何避免略過大量結果。
MongoDB處理不一樣類型的數據是有必定順序的。有時一個鍵的值多是多種類型的,例如,整型和布爾型,或者字符串和null。若是對這種混合類型的鍵排序,其排序順序是預先定義好的。優先級從小到大,其順序以下:
用skip略過少許的文檔仍是不錯的。可是要是數量很是多的話,skip就會變得很慢,由於要先找到須要被略過的數據,而後再拋棄這些數據。大多數數據庫都會在索引中保存更多的元數據,用於處理skip,可是MongoDB目前還不支持,因此要儘可能避免略過太多的數據。一般能夠利用上次的結果來計算下一次查詢條件。
最簡單的分頁方法就是用limit返回結果的第一頁,而後將每一個後續頁面做爲相對於開始的偏移量返回。
> // 不要這麼用:略過的數據比較多時,速度會變得很慢 > var page1 = db.foo.find(criteria).limit(100) > var page2 = db.foo.find(criteria).skip(100).limit(100) > var page3 = db.foo.find(criteria).skip(200).limit(100) ...
然而,通常來說能夠找到一種方法在不使用skip的狀況下實現分頁,這取決於查詢自己。例如,要按照"date"降序顯示文檔列表。能夠用以下方式獲取結果的第一頁:
> var page1 = db.foo.find().sort({"date" : -1}).limit(100)
而後,能夠利用最後一個文檔中"date"的值做爲查詢條件,來獲取下一頁:
var latest = null; // 顯示第一頁 while (page1.hasNext()) { latest = page1.next(); display(latest); } // 獲取下一頁 var page2 = db.foo.find({"date" : {"$gt" : latest.date}}); page2.sort({"date" : -1}).limit(100); 這樣查詢中就沒有skip了。
從集合裏面隨機挑選一個文檔算是個常見問題。最笨的(也很慢的)作法就是先計算文檔總數,而後選擇一個從0到文檔數量之間的隨機數,利用find作一次查詢,略過這個隨機數那麼多的文檔,這個隨機數的取值範圍爲0到集合中文檔的總數:
> // 不要這麼用 > var total = db.foo.count() > var random = Math.floor(Math.random()*total) > db.foo.find().skip(random).limit(1)
這種選取隨機文檔的作法效率過低:首先得計算總數(要是有查詢條件就會很費時),而後用skip略過大量結果也會很是耗時。
略微動動腦筋,從集合裏面查找一個隨機元素仍是有好得多的辦法的。祕訣就是在插入文檔時給每一個文檔都添加一個額外的隨機鍵。例如在shell中,能夠用Math.random()(產生一個0~1的隨機數):
> db.people.insert({"name" : "joe", "random" : Math.random()}) > db.people.insert({"name" : "john", "random" : Math.random()}) > db.people.insert({"name" : "jim", "random" : Math.random()})
這樣,想要從集合中查找一個隨機文檔,只要計算一個隨機數並將其做爲查詢條件就行了,徹底不用skip:
> var random = Math.random() > result = db.foo.findOne({"random" : {"$gt" : random}})
偶爾也會遇到產生的隨機數比集合中全部隨機值都大的狀況,這時就沒有結果返回了。遇到這種狀況,那就將條件操做符換一個方向:
> if (result == null) { ... result = db.foo.findOne({"random" : {"$lt" : random}}) ... }
要是集合裏面本就沒有文檔,則會返回null,這說得通。
這種技巧還能夠和其餘各類複雜的查詢一同使用,僅須要確保有包含隨機鍵的索引便可。例如,想在加州隨機找一個水暖工,能夠對"profession"、"state"和"random"創建索引:
> db.people.ensureIndex({"profession" : 1, "state" : 1, "random" : 1})
這樣就能很快得出一個隨機結果(關於索引,詳見第5章)。
有兩種類型的查詢:簡單查詢(plain query)和封裝查詢(wrapped query)。簡單查詢就像下面這樣:
> var cursor = db.foo.find({"foo" : "bar"})
有一些選項能夠用於對查詢進行「封裝」。例如,假設咱們執行一個排序:
> var cursor = db.foo.find({"foo" : "bar"}).sort({"x" : 1})
實際狀況不是將{"foo" : "bar"}做爲查詢直接發送給數據庫,而是先將查詢封裝在一個更大的文檔中。shell會把查詢從{"foo" : "bar"}轉換成{"$query" : {"foo" : "bar"},"$orderby" : {"x" : 1}}。
絕大多數驅動程序都提供了輔助函數,用於向查詢中添加各類選項。下面列舉了其餘一些有用的選項。
指定本次查詢中掃描文檔數量的上限。
> db.foo.find(criteria)._addSpecial("$maxscan", 20)
若是不但願查詢耗時太多,也不肯定集合中到底有多少文檔須要掃描,那麼可使用這個選項。這樣就會將查詢結果限定爲與被掃描的集合部分相匹配的文檔。這種方式的一個壞處是,某些你但願獲得的文檔沒有掃描到。
查詢的開始條件。在這樣的查詢中,文檔必須與索引的鍵徹底匹配。查詢中會強制使用給定的索引。
在內部使用時,一般應該使用"$gt"代替"$min"。可使用"$min"強制指定一次索引掃描的下邊界,這在複雜查詢中很是有用。
查詢的結束條件。在這樣的查詢中,文檔必須與索引的鍵徹底匹配。查詢中會強制使用給定的索引。
在內部使用時,一般應該使用"$lg"而不是"$max"。可使用"$max"強制指定一次索引掃描的上邊界,這在複雜查詢中很是有用。
在查詢結果中添加一個"$diskLoc"字段,用於顯示該條結果在磁盤上的位置。例如:
> db.foo.find()._addSpecial('$showDiskLoc',true) { "_id" : 0, "$diskLoc" : { "file" : 2, "offset" : 154812592 } } { "_id" : 1, "$diskLoc" : { "file" : 2, "offset" : 154812628 } }
文件號碼顯示了這個文檔所在的文件。若是這裏使用的是test數據庫,那麼這個文檔就在test.2文件中。第二個字段顯示的是該文檔在文件中的偏移量。
數據處理一般的作法就是先把數據從MongoDB中取出來,而後作一些變換,最後再存回去:
cursor = db.foo.find(); while (cursor.hasNext()) { var doc = cursor.next(); doc = process(doc); db.foo.save(doc); }
結果比較少,這樣是沒問題的,可是若是結果集比較大,MongoDB可能會屢次返回同一個文檔。爲何呢?想象一下文檔到底是如何存儲的吧。能夠將集合看作一個文檔列表,如圖4-1所示。雪花表明文檔,由於每個文檔都是美麗且惟一的。
圖4-1 待查詢的集合
這樣,進行查找時,從集合的開頭返回結果,遊標不斷向右移動。程序獲取前100個文檔並處理。將這些文檔保存回數據庫時,若是文檔體積增長了,而預留空間不足,如圖4-2所示,這時就須要對體積增大後的文檔進行移動。一般會將它們挪至集合的末尾處(如圖4-3所示)。
圖4-2 體積變大的文檔,可能沒法保存回原先的位置
圖4-3 MongoDB會爲更新後沒法放回原位置的文檔從新分配存儲空間
如今,程序繼續獲取大量的文檔,如此往復。當遊標移動到集合末尾時,就會返回因體積太大沒法放回原位置而被移動到集合末尾的文檔,如圖4-4所示。
圖4-4 遊標可能會返回那些因爲體積變大而被移動到集合末尾的文檔
應對這個問題的方法就是對查詢進行快照(snapshot)。若是使用了這個選項,查詢就在"_id"索引上遍歷執行,這樣能夠保證每一個文檔只被返回一次。例如,將db.foo.find()改成:
> db.foo.find().snapshot()
快照會使查詢變慢,因此應該只在必要時使用快照。例如,mongodump(用於備份,第22章會介紹)默認在快照上使用查詢。
全部返回單批結果的查詢都被有效地進行了快照。當遊標正在等待獲取下一批結果時,若是集合發生了變化,數據纔可能出現不一致。
看待遊標有兩種角度:客戶端的遊標以及客戶端遊標表示的數據庫遊標。前面討論的都是客戶端的遊標,接下來簡要看看服務器端發生了什麼。
在服務器端,遊標消耗內存和其餘資源。遊標遍歷盡告終果之後,或者客戶端發來消息要求終止,數據庫將會釋放這些資源。釋放的資源能夠被數據庫另做他用,這是很是有益的,因此要儘可能保證儘快釋放遊標(在合理的前提下)。
還有一些狀況致使遊標終止(隨後被清理)。首先,遊標完成匹配結果的迭代時,它會清除自身。另外,若是客戶端的遊標已經不在做用域內了,驅動程序會向服務器發送一條特別的消息,讓其銷燬遊標。最後,即使用戶沒有迭代完全部結果,而且遊標也還在做用域中,若是一個遊標在10分鐘內沒有使用的話,數據庫遊標也會自動銷燬。這樣的話,若是客戶端崩潰或者出錯,MongoDB就不須要維護這上千個被打開卻再也不使用的遊標。
這種「超時銷燬」的行爲是咱們但願的:極少有應用程序但願用戶花費數分鐘坐在那裏等待結果。然而,有時的確但願遊標持續的時間長一些。如果如此的話,多數驅動程序都實現了一個叫immortal的函數,或者相似的機制,來告知數據庫不要讓遊標超時。若是關閉了遊標的超時時間,則必定要迭代完全部結果,或者主動將其銷燬,以確保遊標被關閉。不然它會一直在數據庫中消耗服務器資源。
有一種很是特殊的查詢類型叫做數據庫命令(database command)。前面已經介紹過文檔的建立、更新、刪除以及查詢。這些都是數據庫命令的使用範疇,包括管理性的任務(好比關閉服務器和克隆數據庫)、統計集合內的文檔數量以及執行聚合等。
本節主要講述數據庫命令,在數據操做、管理以及監控中,數據庫命令都是很是有用的。例如,刪除集合是使用"drop"數據庫命令完成的:
> db.runCommand({"drop" : "test"}); { "nIndexesWas" : 1, "msg" : "indexes dropped for collection", "ns" : "test.test", "ok" : true }
也許你對shell輔助函數比較熟悉,這些輔助函數封裝數據庫命令,並提供更加簡單的接口:
> db.test.drop()
一般,只使用shell輔助函數就能夠了,可是瞭解它們底層的命令頗有幫助。尤爲是當使用舊版本的shell鏈接到新版本的數據庫上時,這個shell可能不支持新版數據庫的一些命令,這時候就不得不直接使用runCommand()。
在前面的章節中已經看到過一些命令了,好比,第3章使用getLastError來查看更新操做影響到的文檔數量:
> db.count.update({x : 1}, {$inc : {x : 1}}, false, true) > db.runCommand({getLastError : 1}) { "err" : null, "updatedExisting" : true, "n" : 5, "ok" : true }
本節會更深刻地介紹數據庫命令,一塊兒來看看這些數據庫命令究竟是什麼,究竟是怎麼實現的。本節也會介紹MongoDB提供的一些很是有用的命令。在shell中運行db.listCommands()能夠看到全部的數據庫命令。
數據庫命令總會返回一個包含"ok"鍵的文檔。若是"ok"的值是1,說明命令執行成功了;若是值是0,說明因爲一些緣由,命令執行失敗。
若是"ok"的值是0,那麼命令的返回文檔中就會有一個額外的鍵"errmsg"。它的值是一個字符串,用於描述命令的失敗緣由。例如,若是試着在上一節已經刪除的集合上再次執行drop命令:
> db.runCommand({"drop" : "test"}); { "errmsg" : "ns not found", "ok" : false }
MongoDB中的命令被實現爲一種特殊類型的查詢,這些特殊的查詢會在$cmd集合上執行。runCommand只是接受一個命令文檔,而且執行與這個命令文檔等價的查詢。因而,drop命令會被轉換爲以下代碼:
db.$cmd.findOne({"drop" : "test"});
當MongoDB服務器獲得一個在$cmd集合上的查詢時,不會對這個查詢進行一般的查詢處理,而是會使用特殊的邏輯對其進行處理。幾乎全部的MongoDB驅動程序都會提供一個相似runCommand的輔助函數,用於執行命令,並且命令老是可以以簡單查詢的方式執行。
有些命令須要有管理員權限,並且要在admin數據庫上才能執行。若是在其餘數據庫上執行這樣的命令,就會獲得一個"access denied"(訪問被拒絕)錯誤。若是當前位於其餘的數據庫,可是須要執行一個管理員命令,可使用adminCommand而不是runCommand:
> use temp switched to db temp > db.runCommand({shutdown:1}) { "errmsg" : "access denied; use admin db", "ok" : 0 } > db.adminCommand({"shutdown" : 1})
MongoDB中,數據庫命令是少數與字段順序相關的地方之一:命令名稱必須是命令中的第一個字段。所以, {"getLastError" : 1, "w" : 2}是有效的命令,而{"w" : 2, "getLastError" : 1}不是。
上一篇文章: MongoDB指南---八、特定類型的查詢
下一篇文章: MongoDB指南---十、索引、複合索引 簡介