MongoDB 索引技巧 : 太多字段要索引怎么办?使用通用索引

问题

当你的文档包含很多不同的字段,并且你需要根据这些字段进行高效的检索。例如下面一个文档描述了一个人:

{
    _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()函数,该函数可以指定多个键。

  1. > db.things.ensureIndex({j:1,name:-1}) 

当创建索引时,键后面的数字表明了索引的方向,取值为1或者-1,1表示升序,-1表示降序。升序或者降序在随机访问的时候关系不大,当时在做排序或者范围查询的时候就很重要了。

如果在建立了a,b,c这样一个复合索引,那么你可以在a,A,b和a,b,c上使用索引查询。

 

 

 

稀疏索引

和稀疏矩阵类似,稀疏索引就是索引至包含被索引字段的文档。

任何一个稀疏的缺失某一个字段的文档将不会存储在索引中,之所以称之为稀疏索引就是说缺失字段的文档的值会丢失。

稀疏索引的创建和完全索引的创建没有什么不同。使用稀疏索引进行查询的时候,某些由于缺失了字段的文档记录可能不会被返回,这是由于稀疏索引子返回被索引了的字段。可能比较难以理解,不过看几个例子就好理解了。

  1. > db.people.ensureIndex({title:1},{sparse:true}) //在title字段上建立稀疏索引  
  2.  
  3. > db.people.save({name:"Jim"})  
  4.  
  5. > db.people.save({name:"yang",title:"prince"})  
  6.  
  7. > db.people.find();  
  8.  
  9. { "_id" : ObjectId("4e244dc5cac1e3490b9033d7"), "name" : "Jim" }  
  10.  
  11. { "_id" : ObjectId("4e244debcac1e3490b9033d8"), "name" : "yang", "title" : "prince" }  
  12.  
  13. > db.people.find().sort({title:1})//自有包含有索引字段的记录才被返回  
  14.  
  15. { "_id" : ObjectId("4e244debcac1e3490b9033d8"), "name" : "yang", "title" : "prince" }  
  16.  
  17. > db.people.dropIndex({title:1})//删除稀疏索引之后,所有的记录均显示  
  18.  
  19. { "nIndexesWas" : 2, "ok" : 1 }  
  20.  
  21. > db.people.find().sort({title:1})  
  22.  
  23. { "_id" : ObjectId("4e244dc5cac1e3490b9033d7"), "name" : "Jim" }  
  24.  
  25. { "_id" : ObjectId("4e244debcac1e3490b9033d8"), "name" : "yang", "title" : "prince" }

结论

结论是, 你可以看到MongoDB在2.4版本提供了一个简单有效的方式在很多的属性上建立通用索引. 现在你可以在你所有的拥有很多属性的大数据项目中自由的索引和查询了 :)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章