問題
當你的文檔包含很多不同的字段,並且你需要根據這些字段進行高效的檢索。例如下面一個文檔描述了一個人:
{
_id: 123,
firstName: "John",
lastName: "Smith",
age: 25,
height: 6.0,
dob: Date,
eyes: "blue",
sign: "Capricorn",
...
}
假設你需要查找出所有藍眼睛的、特定高度以及某姓的人,可能有很多這樣文檔有這些屬性,也可能你根本不知道會有什麼,或者文檔本身的差異非常大。那麼你如何使用索引來快速解決這個需求呢?很顯然這個查詢的消耗是非常大的,如果在每個字段都創建一個索引也是不切實際的。
方案 #1: 基於鍵值對的複合索引
讓我們先從schema的設計出發,利用JSON通過使用列表來存儲所有的屬性:
{
_id: 123,
props: [
{ n: "firstName", v: "John"},
{ n: "lastName", v: "Smith"},
{ n: "age", v: 25},
...
]
}
這裏創建的索引是一個基於name和value字段的複合索引。讓我們創建數百萬個包含了值爲0至100的隨機數值的僞造屬性的文檔。
> for (var i = 0; i < 5000000; ++i) { var arr = []; for (var j = 0; j < 10; ++j) {
arr.push({n: "prop" + j, v: Math.floor(Math.random() * 1000) }) };
db.generic.insert({props: arr}) }
> db.generic.findOne()
{
"_id": ObjectId("515dd3b4f0bd676b816aa9b0"),
"props": [
{
"n": "prop0",
"v": 40
},
{
"n": "prop1",
"v": 198
},
...
{
"n": "prop9",
"v": 652
}
]
}
> db.generic.ensureIndex({"props.n": 1, "props.v": 1})
> db.generic.stats()
{
"ns": "test.generic",
"count": 5020473,
"size": 1847534064,
"avgObjSize": 368,
"storageSize": 2600636416,
"numExtents": 19,
"nindexes": 2,
"lastExtentSize": 680280064,
"paddingFactor": 1,
"systemFlags": 1,
"userFlags": 0,
"totalIndexSize": 1785352240,
"indexSizes": {
"_id_": 162898624,
"props.n_1_props.v_1": 1622453616
},
"ok": 1
}
正如你所看到的,索引的大小有1.6G之巨,因爲我們將name和value屬性和值都存儲在了索引中。這完全只是獲取一個索引的代價!現在我們繼續查詢操作...查找"prop1"爲0的所有文檔:
> db.generic.findOne({"props.n": "prop1", "props.v": 0})
{
"_id": ObjectId("515dd4298bff7c34610f6ae8"),
"props": [
{
"n": "prop0",
"v": 788
},
{
"n": "prop1",
"v": 0
},
...
{
"n": "prop9",
"v": 788
}
]
}
> db.generic.find({"props.n": "prop1", "props.v": 0}).explain()
{
"cursor": "BtreeCursor props.n_1_props.v_1",
"isMultiKey": true,
"n": 49822,
"nscannedObjects": 5020473,
"nscanned": 5020473,
"nscannedObjectsAllPlans": 5020473,
"nscannedAllPlans": 5020473,
"scanAndOrder": false,
"indexOnly": false,
"nYields": 0,
"nChunkSkips": 0,
"millis": 252028,
"indexBounds": {
"props.n": [
[
"prop1",
"prop1"
]
],
"props.v": [
[
{
"$minElement": 1
},
{
"$maxElement": 1
}
]
]
},
"server": "agmac.local:27017"
}
以上的查詢未能獲得正確的結果:命中了5w條記錄且耗時252s!原因在於,對於每次的查詢語句,n=”prop1”和v=0不必校驗兩個條件是否在同一個嵌套文檔中同時成立而只要在同一個文檔中同時成立即可。基本上說就是n和v的條件組合在一個文檔中匹配了超出範圍的數據。當然你可以抱怨查詢語句選擇上的歧義,要讓查詢強制匹配嵌套文檔的方式是使用“$elemMatch”:
> db.generic.findOne({"props": { $elemMatch: {n: "prop1", v: 0} }})
現在,讓我們看看MongoDB v2.2是怎樣使用複合索引以及使用複合索引後的耗時情況:
> db.generic.find({"props": { $elemMatch: {n: "prop1", v: 0} }}).explain()
{
"cursor": "BtreeCursor props.n_1_props.v_1",
"isMultiKey": true,
"n": 5024,
"nscannedObjects": 5020473,
"nscanned": 5020473,
"nscannedObjectsAllPlans": 5020473,
"nscannedAllPlans": 5020473,
"scanAndOrder": false,
"indexOnly": false,
"nYields": 0,
"nChunkSkips": 0,
"millis": 278784,
"indexBounds": {
"props.n": [
[
"prop1",
"prop1"
]
],
"props.v": [
[
{
"$minElement": 1
},
{
"$maxElement": 1
}
]
]
},
"server": "agmac.local:27017"
}
現在能返回正確的5024個文檔了...但是速度仍然很慢!就像你在執行計劃中看到的那樣,原因在於字段v的取值範圍仍然是開放區間的。爲什麼會這樣?讓我們回退幾秒:如果不使用$elemMatch,那麼這兩個字段的所有組合將會被匹配上。然而對於建立這樣的索引來說相應的元素組合將會是龐大的。所以MongoDB選擇了將一個嵌套文檔中的值放在了同一個B樹的Bucket上且忽略了兩個字段的組合情況(類似$elemMatch的做法)。但是爲什麼使用$elemMatch的查詢仍然很慢?這是一個在v2.4中已經被修復的缺陷,見SERVER-3104。升級至2.4版本,你會看到:
> db.generic.find({"props": { $elemMatch: {n: "prop1", v: 0} }}).explain()
{
"cursor": "BtreeCursor props.n_1_props.v_1",
"isMultiKey": true,
"n": 5024,
"nscannedObjects": 5024,
"nscanned": 5024,
"nscannedObjectsAllPlans": 5024,
"nscannedAllPlans": 5024,
"scanAndOrder": false,
"indexOnly": false,
"nYields": 0,
"nChunkSkips": 0,
"millis": 21,
"indexBounds": {
"props.n": [
[
"prop1",
"prop1"
]
],
"props.v": [
[
0,
0
]
]
},
"server": "agmac.local:27017"
}
好了現在我們將查詢速率提升至21毫秒,這才靠譜嘛!接着讓我們分別使用$all/$in操作符在查詢中進行“與/或”查詢。注意$all操作符只會使用第一個元素遍歷索引樹,所以如果知道條件的限制程度則優先將限制程度高的條件放在首位。
db.generic.find({"props": { $all: [{ $elemMatch: {n: "prop1", v: 0} },{ $elemMatch: {n: "prop2", v: 63} } ]}})
警告:在複合索引上的範圍查詢不會正確地限制索引的邊界,會導致掃描不必要的數據。這個bug SERVER-10436 有望會在v2.6中修復:
> db.generic.find({ props: { $elemMatch: {n: "prop1", v: { $gte: 6, $lte: 9 } }}}).explain()
{
"cursor" : "BtreeCursor props.n_1_props.v_1",
"isMultiKey" : true,
"n" : 506,
"nscannedObjects" : 126571,
"nscanned" : 126571,
"nscannedObjectsAllPlans" : 126571,
"nscannedAllPlans" : 126571,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 1,
"nChunkSkips" : 0,
"millis" : 1396,
"indexBounds" : {
"props.n" : [
[
"prop1",
"prop1"
]
],
"props.v" : [
[
6,
1.7976931348623157e+308
]
]
},
"server" : "agmac.local:27017"
}
方案 #2: 單列BLOB索引
另外一種解決索引多列問題的方法是將“屬性:值”對放在一個子對象列表中。這個方式適用於v2.2和v2.4版本。建立如下的文檔:
> for (var i = 0; i < 5000000; ++i) { var arr = []; for (var j = 0; j < 10; ++j) {
var doc = {}; doc["prop" + j] = Math.floor(Math.random() * 1000); arr.push(doc) };
db.generic2.insert({props: arr}) }
> db.generic2.findOne()
{
"_id": ObjectId("515e5e6a71b0722678929760"),
"props": [
{
"prop0": 881
},
{
"prop1": 47
}, ...
{
"prop9": 717
}
]
}
索引應該建立在列表上,因爲屬性名稱是可變的:
> db.generic2.ensureIndex({props: 1})
> db.generic2.stats()
{
"ns": "test.generic2",
"count": 5000000,
"size": 1360000032,
"avgObjSize": 272.0000064,
"storageSize": 1499676672,
"numExtents": 19,
"nindexes": 2,
"lastExtentSize": 393670656,
"paddingFactor": 1,
"systemFlags": 1,
"userFlags": 0,
"totalIndexSize": 2384023488,
"indexSizes": {
"_id_": 162269072,
"props_1": 2221754416
},
"ok": 1
}
正如你所看到的那樣,索引比方案一中的複合索引還要大30%,因爲BSON格式的嵌套文檔被以BLOB的格式存儲在了索引中。繼續進行查詢:
> db.generic2.find({"props": {"prop1": 0} }).explain()
{
"cursor": "BtreeCursor props_1",
"isMultiKey": true,
"n": 4958,
"nscannedObjects": 4958,
"nscanned": 4958,
"nscannedObjectsAllPlans": 4958,
"nscannedAllPlans": 4958,
"scanAndOrder": false,
"indexOnly": false,
"nYields": 0,
"nChunkSkips": 0,
"millis": 15,
"indexBounds": {
"props": [
[
{
"prop1": 0
},
{
"prop1": 0
}
]
]
},
"server": "agmac.local:27017"
}
結果查詢速率比方案1還要快,只有15毫秒!但是有一個地方需要注意的是查詢謂詞必須使用一整個的JSON對象。需要匹配prop1從0至9的記錄,則查詢將爲:
> db.generic2.find({"props": { $elemMatch: { $gte: {"prop1": 0}, $lte: {"prop1": 9} }})
如果在子對象還有其他的字段需要匹配(記住,子對象僅僅是一個用於MongoDB的BLOB)時, 這些(字段)必須是JSON查詢謂詞的一部分。比方說現在你需要一個開放的範圍如:存在“prop1”並且大於6, 你還應該指定一個上限, 否則它會比預期匹配更多的文件。理想情況下,你可以使用MaxKey爲上限. 但是我還發現了一個BUG SERVER-10394 其中約束的類型必須指定爲相同類型。
db.generic2.find({"props": { $elemMatch: {$gte: {"prop1": 6}, $lt: {"prop1": 99999999 } }}})
一個需要注意的地方:你不能單獨用(字段的)值做索引。例如在 方案# 1中 ,如果你想找到任何具有屬性值爲10的文檔,你只需要根據“props.v”創建索引。這個(索引)不可能在 方案 #2 中 根據字段名變化 。
組合關鍵字索引
除了基本的以單個關鍵字作爲索引外,MongoDB也支持多個關鍵字的組合索引,和基本的索引一樣,也是用ensureIndex()函數,該函數可以指定多個鍵。
- > db.things.ensureIndex({j:1,name:-1})
當創建索引時,鍵後面的數字表明瞭索引的方向,取值爲1或者-1,1表示升序,-1表示降序。升序或者降序在隨機訪問的時候關係不大,當時在做排序或者範圍查詢的時候就很重要了。
如果在建立了a,b,c這樣一個複合索引,那麼你可以在a,A,b和a,b,c上使用索引查詢。
稀疏索引
和稀疏矩陣類似,稀疏索引就是索引至包含被索引字段的文檔。
任何一個稀疏的缺失某一個字段的文檔將不會存儲在索引中,之所以稱之爲稀疏索引就是說缺失字段的文檔的值會丟失。
稀疏索引的創建和完全索引的創建沒有什麼不同。使用稀疏索引進行查詢的時候,某些由於缺失了字段的文檔記錄可能不會被返回,這是由於稀疏索引子返回被索引了的字段。可能比較難以理解,不過看幾個例子就好理解了。
- > db.people.ensureIndex({title:1},{sparse:true}) //在title字段上建立稀疏索引
- > db.people.save({name:"Jim"})
- > db.people.save({name:"yang",title:"prince"})
- > db.people.find();
- { "_id" : ObjectId("4e244dc5cac1e3490b9033d7"), "name" : "Jim" }
- { "_id" : ObjectId("4e244debcac1e3490b9033d8"), "name" : "yang", "title" : "prince" }
- > db.people.find().sort({title:1})//自有包含有索引字段的記錄才被返回
- { "_id" : ObjectId("4e244debcac1e3490b9033d8"), "name" : "yang", "title" : "prince" }
- > db.people.dropIndex({title:1})//刪除稀疏索引之後,所有的記錄均顯示
- { "nIndexesWas" : 2, "ok" : 1 }
- > db.people.find().sort({title:1})
- { "_id" : ObjectId("4e244dc5cac1e3490b9033d7"), "name" : "Jim" }
- { "_id" : ObjectId("4e244debcac1e3490b9033d8"), "name" : "yang", "title" : "prince" }
結論
結論是, 你可以看到MongoDB在2.4版本提供了一個簡單有效的方式在很多的屬性上建立通用索引. 現在你可以在你所有的擁有很多屬性的大數據項目中自由的索引和查詢了 :)