MongoDB指南---九、遊標與數據庫命令

上一篇文章: 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請求包含一個查詢標識符,向數據庫詢問是否還有更多的結果,若是有,則返回下一批結果。這個過程會一直持續到遊標耗盡或者結果所有返回。服務器

4.5.1 limit、skip和sort

最經常使用的查詢選項就是限制返回結果的數量、忽略必定數量的結果以及排序。全部這些選項必定要在查詢被髮送到服務器以前指定。
要限制結果數量,可在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。若是對這種混合類型的鍵排序,其排序順序是預先定義好的。優先級從小到大,其順序以下:

  1. 最小值;
  2. null;
  3. 數字(整型、長整型、雙精度);
  4. 字符串;
  5. 對象/文檔;
  6. 數組;
  7. 二進制數據;
  8. 對象ID;
  9. 布爾型;
  10. 日期型;
  11. 時間戳;
  12. 正則表達式;
  13. 最大值 。

4.5.2 避免使用skip略過大量結果

用skip略過少許的文檔仍是不錯的。可是要是數量很是多的話,skip就會變得很慢,由於要先找到須要被略過的數據,而後再拋棄這些數據。大多數數據庫都會在索引中保存更多的元數據,用於處理skip,可是MongoDB目前還不支持,因此要儘可能避免略過太多的數據。一般能夠利用上次的結果來計算下一次查詢條件。

1. 不用skip對結果分頁

最簡單的分頁方法就是用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了。

2. 隨機選取文檔

從集合裏面隨機挑選一個文檔算是個常見問題。最笨的(也很慢的)作法就是先計算文檔總數,而後選擇一個從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章)。

4.5.3 高級查詢選項

有兩種類型的查詢:簡單查詢(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}}。
絕大多數驅動程序都提供了輔助函數,用於向查詢中添加各類選項。下面列舉了其餘一些有用的選項。

  • $maxscan : integer

指定本次查詢中掃描文檔數量的上限。

> db.foo.find(criteria)._addSpecial("$maxscan", 20)

若是不但願查詢耗時太多,也不肯定集合中到底有多少文檔須要掃描,那麼可使用這個選項。這樣就會將查詢結果限定爲與被掃描的集合部分相匹配的文檔。這種方式的一個壞處是,某些你但願獲得的文檔沒有掃描到。

  • $min : document

查詢的開始條件。在這樣的查詢中,文檔必須與索引的鍵徹底匹配。查詢中會強制使用給定的索引。
在內部使用時,一般應該使用"$gt"代替"$min"。可使用"$min"強制指定一次索引掃描的下邊界,這在複雜查詢中很是有用。

  • $max : document

查詢的結束條件。在這樣的查詢中,文檔必須與索引的鍵徹底匹配。查詢中會強制使用給定的索引。
在內部使用時,一般應該使用"$lg"而不是"$max"。可使用"$max"強制指定一次索引掃描的上邊界,這在複雜查詢中很是有用。

  • $showDiskLoc : true

在查詢結果中添加一個"$diskLoc"字段,用於顯示該條結果在磁盤上的位置。例如:

> db.foo.find()._addSpecial('$showDiskLoc',true)
{ "_id" : 0, "$diskLoc" : { "file" : 2, "offset" : 154812592 } }
{ "_id" : 1, "$diskLoc" : { "file" : 2, "offset" : 154812628 } }

文件號碼顯示了這個文檔所在的文件。若是這裏使用的是test數據庫,那麼這個文檔就在test.2文件中。第二個字段顯示的是該文檔在文件中的偏移量。

4.5.4 獲取一致結果

數據處理一般的作法就是先把數據從MongoDB中取出來,而後作一些變換,最後再存回去:

cursor = db.foo.find();

while (cursor.hasNext()) {
    var doc = cursor.next();
    doc = process(doc);
    db.foo.save(doc);
}

結果比較少,這樣是沒問題的,可是若是結果集比較大,MongoDB可能會屢次返回同一個文檔。爲何呢?想象一下文檔到底是如何存儲的吧。能夠將集合看作一個文檔列表,如圖4-1所示。雪花表明文檔,由於每個文檔都是美麗且惟一的。

clipboard.png

圖4-1 待查詢的集合
這樣,進行查找時,從集合的開頭返回結果,遊標不斷向右移動。程序獲取前100個文檔並處理。將這些文檔保存回數據庫時,若是文檔體積增長了,而預留空間不足,如圖4-2所示,這時就須要對體積增大後的文檔進行移動。一般會將它們挪至集合的末尾處(如圖4-3所示)。

clipboard.png

圖4-2 體積變大的文檔,可能沒法保存回原先的位置

clipboard.png

圖4-3  MongoDB會爲更新後沒法放回原位置的文檔從新分配存儲空間
如今,程序繼續獲取大量的文檔,如此往復。當遊標移動到集合末尾時,就會返回因體積太大沒法放回原位置而被移動到集合末尾的文檔,如圖4-4所示。

clipboard.png

圖4-4 遊標可能會返回那些因爲體積變大而被移動到集合末尾的文檔
應對這個問題的方法就是對查詢進行快照(snapshot)。若是使用了這個選項,查詢就在"_id"索引上遍歷執行,這樣能夠保證每一個文檔只被返回一次。例如,將db.foo.find()改成:

> db.foo.find().snapshot()

快照會使查詢變慢,因此應該只在必要時使用快照。例如,mongodump(用於備份,第22章會介紹)默認在快照上使用查詢。
全部返回單批結果的查詢都被有效地進行了快照。當遊標正在等待獲取下一批結果時,若是集合發生了變化,數據纔可能出現不一致。

4.5.5 遊標生命週期

看待遊標有兩種角度:客戶端的遊標以及客戶端遊標表示的數據庫遊標。前面討論的都是客戶端的遊標,接下來簡要看看服務器端發生了什麼。
在服務器端,遊標消耗內存和其餘資源。遊標遍歷盡告終果之後,或者客戶端發來消息要求終止,數據庫將會釋放這些資源。釋放的資源能夠被數據庫另做他用,這是很是有益的,因此要儘可能保證儘快釋放遊標(在合理的前提下)。
還有一些狀況致使遊標終止(隨後被清理)。首先,遊標完成匹配結果的迭代時,它會清除自身。另外,若是客戶端的遊標已經不在做用域內了,驅動程序會向服務器發送一條特別的消息,讓其銷燬遊標。最後,即使用戶沒有迭代完全部結果,而且遊標也還在做用域中,若是一個遊標在10分鐘內沒有使用的話,數據庫遊標也會自動銷燬。這樣的話,若是客戶端崩潰或者出錯,MongoDB就不須要維護這上千個被打開卻再也不使用的遊標。
這種「超時銷燬」的行爲是咱們但願的:極少有應用程序但願用戶花費數分鐘坐在那裏等待結果。然而,有時的確但願遊標持續的時間長一些。如果如此的話,多數驅動程序都實現了一個叫immortal的函數,或者相似的機制,來告知數據庫不要讓遊標超時。若是關閉了遊標的超時時間,則必定要迭代完全部結果,或者主動將其銷燬,以確保遊標被關閉。不然它會一直在數據庫中消耗服務器資源。

4.6 數據庫命令

有一種很是特殊的查詢類型叫做數據庫命令(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指南---十、索引、複合索引 簡介
相關文章
相關標籤/搜索