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

问题

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

{
    _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在2.4版本提供了一个简单有效的方式在很多的属性上建立通用索引. 现在你可以在你所有的拥有很多属性的大数据项目中自由的索引和查询了 :)
















你可能感兴趣的:(mongodb,索引,大数据,对象,存储)