转载请出自出处:http://eksliang.iteye.com/blog/2178555
一、概述
上一篇文档中也说明了,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" : false | MongoDB是否在内存中对结果集进行了排序 |
"indexOnly" : false | MongoDB是否只使用索引就可以完成本次查询("覆盖索引") |
"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个结果的就是胜者,其他查询计划都会被终止。
这个查询计划会被缓冲,接下来的这个查询都会使用他,下面几种情况会重新计划;
- 最初的计划评估之后集合发生了比较大的数据波动,查询优化器就会重新挑选可行的查询计划。
- 建立索引时。
- 每执行1000次查询之后,查询优化器就会重新评估查询计划
八、何时不应该使用索引
提取较小的子数据集时,索引非常有效(所以才有了分页)。也有一些查询不使用索引会更快。结果集在原集合中所占的比例越大,查询效率越慢。因为使用索引需要进行两次查找:一次查找索引条目,一次根据索引指针去查找相应的文档。而全表扫描只需要进行一次查询。在最坏的情况,使用索引进行查找次数会是全表扫描的两倍。效率会明显比全表扫描低。
可惜并没有一个严格的规则可以告诉我们,如果根据索引大小、文档大小来判断什么时候索引很有用,一般来说,如果查询需要返回集合内30%的文档(或者更多),那就应该测试全表扫描和走索引查询那个速度比较快。这个数字也会在2%~60%之间进行波动。
这个时候可以使用hint({"$natural":true})强制查询走全表扫描。