MongoDB索引调优(2)——[十]

一、概述

      上一篇文档中也说明了,MongoDB的索引几乎与关系型数据库的索引一模一样,优化关系型数据库的技巧通用适合MongoDB,所有这里只讲MongoDB需要注意的地方

二、索引内嵌文档

    可以在嵌套文档的键上建立索引,方式与正常的键一样。如果有这样一个集合,如下所示:

db.emp.insert({
	"_id":"A001",
	"name":{
		"first":"Carey",
		"last":"Ickes"
	},
	"age":25
})

   现在我要在内嵌文档的first键上建立索引?如下所示

> db.emp.ensureIndex({"name.first":1})

      温馨提示:对嵌套文档本身“name”建立索引,与对嵌套文档的某个字段(name.first)建立索引是完全不相同的,对整个文档建立索引,只有在使用文档完整匹配时才会使用到这个索引,例如建立了这样一个索引db.emp.ensureIndex({"name":1}),那么只有使用db.emp.find({"name":{"first":"xxx","last":"xxx"}})这种完整匹配时才会使用到这个索引,使用db.emp.find({"name.first":"xxx"})是不会使用到该索引的。

三、索引数组

      MongoDB支持对数组建立索引,这样就可以高效的搜索数组中的特定元素。例如有一个博客文章集合,其中每个文档都表示一篇文章。每篇文章都有一个comments字段,这是一个数组,用来保存别人对这篇文章的评论信息。blog集合结构如下:

db.blog.insert({
	"_id":"B001",
	"title":"MongoDB查询",
	"comments":[
	  {"name":"ickes","score":3,"comment":"nice"},
	  {"name":"xl","score":4,"comment":"nice"},
	  {"name":"eksliang","score":5,"comment":"nice"},
	  {"name":"ickes","score":6,"comment":"nice"}
	]
})

   如果要找出comments.score大于5的所有文章,可以在comments.score键上面建立索引:

> db.blog.ensureIndex({"comments.score":1})

   对数组建立索引需要注意   

  • 对数组建立索引,实际上是对数组的每一个元素建立一个索引条目,如果一篇文章有20个评论,那么它就拥有20个索引条目。因此索引的代价比较单值索引的代价高。
  • 在数组上建立的索引并不包含任何位置信息:无法使用数组索引查找特定位置的数组元素,比如"comments.4".
  • 一个索引中数组字段最多只能有一个。这是为了避免在复合索引中索引条目爆炸性增长。
  • 对于某个索引的键,如果这个键在某个文档中是一个数组,那么这个索引就会标记为多键索引。可以从explain()的输出信息中看到一个索引是否为多键索引。如果是多键索引,那么"isMultikey"字段的值就会为true。索引只要被标记为多键索引,就无法再变成非多键索引,即使索引的键从文档中删除也不行。唯一的办法就是删除重建。

四、explain()和hint()

         explian()能够提供大量与查询相关的信息。对于速度比较慢的查询来说,这是最重要的诊断工具。通过查看explian()的输出信息,可以知道查询使用了那个索引,以及如果使用他的。对于任意查询来,都可以在最后添加一个explain()调用,但是调用explain()的时间必须是最后。

参考实例:

> db.users.find({"age":{"$gt":10}}).sort({"age":1}).limit(100).explain()
{
        "cursor" : "BtreeCursor age_1_name_1",
        "isMultiKey" : false,
        "n" : 100,
        "nscannedObjects" : 100,
        "nscanned" : 100,
        "nscannedObjectsAllPlans" : 100,
        "nscannedAllPlans" : 100,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 0,
        "indexBounds" : {
                "age" : [
                        [
                                10,
                                Infinity
                        ]
                ],
                "name" : [
                        [
                                {
                                        "$minElement" : 1
                                },
                                {
                                        "$maxElement" : 1
                                }
                        ]
                ]
        },
        "server" : "localhost.localdomain:27017",
        "filterSet" : false
}

    explain()输出各个信息的含义如下表所示

"cursor" : "BtreeCursor age_1_name_1"表示本次查询使用了索引,具体使用了age_1_name_1这个索引
"isMultiKey" : false用于说明本次查询是否使用了多键索引
"n" : 100本次查询返回的文档数量
"nscannedObjects" : 100这是MongoDB使用索引指针去磁盘上查找文档的实际次数
"nscanned" : 100如果有使用索引,那么这个数字就是检查过的索引个数,如果本次查询是全表扫描,那么这个数字就是检查过的文档个数
"scanAndOrder" : falseMongoDB是否在内存中对结果集进行了排序
 "indexOnly" : falseMongoDB是否只使用索引就可以完成本次查询("覆盖索引")
 "nYields" : 0为了让写入请求能够顺利执行,本次查询暂停的次数。如果有写入请求需要处理,查询会周期性地释放他的锁,以便写人能够顺利执行。然而,在本次查询中没有写入请求,因为查询没有暂停过。
"millis" : 0数据库执行本次查询所耗费的毫秒数。这个数字越小,说明查询效率越高
indexBounds:{...}这个字段描述了索引的使用情况,给出了索引的遍历范围。因为本次查询只使用了age做为查询条件跟排序条件,没有指定第二个键,因此在name键上没有限制,数据库会把用户名介于负无穷("$minElement" : 1)到正无穷("$maxElement" : 1)之间进行搜索

hint()方法用于让MongoDB强制使用某个索引,或者查询使用了索引反而效率更低,这个时候可以使用hint()屏蔽索引让查询走全表扫描。

参考实例:强制本次查询使用{"name":1,"age":1}这个索引

> db.users.find({"age":{"$gt":10}}).sort({"age":1}).hint({"name":1,"age":1}).explain()

 参考实例:强制本次查询走全表扫描

> db.users.find({"age":{"$gt":10}}).sort({"age":1}).hint({"$natural":true}).explain()--使用{"$natural":1}--$natural 强制全表扫描

 五、MongoDB中低效率的操作符

"$where"和"$exists":这两个操作符,完全不能使用索引。

"$ne":通常来说取反的效率比较低。"$ne"查询可以使用索引,但并不是很有效。因为他必须查看所有的索引条目,而不是"$ne"指定的条目,这个时候他就不得不扫描整个索引。

"$not":有时候能够使用索引,但是他通常并不知道要如何使用索引。所以大多数情况"$not"会退化为全表扫描。

"$nin":这个操作符总是会全表扫描

六、OR查询

         MongoDB在一次查询中只能使用一个索引(至少我现在用的2.6是这样的),如果你在{"x":1}上有一个索引,在{"y":1}上也有一个索引,在{"x":1,"y":1}上执行查询时,MongoDB只会使用其中一个索引,而不是两个一起使用。"$or"是一个例外,"$or"可以对每个字句都使用索引,因为"$or"实际上是执行两次查询然后将结果合并。

         通常来说,使用or查询多次在合并结果,不如单次查询的效率高,对于单个字段,应该尽可能使用$in。

七、MongoDB的查询优化器

      MongoDB的查询优化器与其他数据库的稍微不同。基本来说,如果一个索引能够精确匹配一个查询,那么查询优化器就会使用这个索引,如果不能精确匹配,可能会有几个索引都适合你的查询。那MongoDB是怎样选择的呢?答:MongoDB的查询计划会将多个索引并行的去执行,最早返回100个结果的就是胜者,其他查询计划都会被终止。

      这个查询计划会被缓冲,接下来的这个查询都会使用他,下面几种情况会重新计划;

  1. 最初的计划评估之后集合发生了比较大的数据波动,查询优化器就会重新挑选可行的查询计划。
  2. 建立索引时。
  3. 每执行1000次查询之后,查询优化器就会重新评估查询计划

八、何时不应该使用索引

      提取较小的子数据集时,索引非常有效(所以才有了分页)。也有一些查询不使用索引会更快。结果集在原集合中所占的比例越大,查询效率越慢。因为使用索引需要进行两次查找:一次查找索引条目,一次根据索引指针去查找相应的文档。而全表扫描只需要进行一次查询。在最坏的情况,使用索引进行查找次数会是全表扫描的两倍。效率会明显比全表扫描低。

     可惜并没有一个严格的规则可以告诉我们,如果根据索引大小、文档大小来判断什么时候索引很有用,一般来说,如果查询需要返回集合内30%的文档(或者更多),那就应该测试全表扫描和走索引查询那个速度比较快。这个数字也会在2%~60%之间进行波动。

这个时候可以使用hint({"$natural":true})强制查询走全表扫描。