索引介绍

给 username 添加索引

  1. > db.users.ensureIndex({"username":1})

索引可以让 MongoDB 能够非常快的找到文档。但是对于添加的每个索引,每次写操作(插入、更新、删除)都将花费更多的时间。在每个集合里最多只能有 64 个索引,在一个特定集合上,不应改有两个以上的索引。除非这个键经常会用到,否则不要添加索引。

当使用复合索引进行排序操作时,请将需要排序的键放在第一个

  1. > db.users.ensureIndex({"age" : 1,"username" : 1})
  2. > db.users.find({"age" : {"$gte":21,"$lte":30})
  3. .sort({"username":1}).hint({"username":1,"age":1})

使用复合索引

键的方向

索引使用的方向与排序方向相同,互相反转(在每个方向上都乘上 -1)的索引是等价的:{“username”:1,”age”:-1} 和 {“username”:-1,”age”:1} 是完全一样的。只有基于多个索引进行排序时,索引方向才是比较重要的,只是基于一个键进行排序,MongoDB 可以从正反两方向读取索引。只有在多键排序时,方向才重要。

使用覆盖索引

如果你的查询只需要索引中包含的字段,不需要获取整个文档。当一个索引覆盖用户所需的所有字段,可以认为这个索引覆盖了本次查询。在实际中,优先使用覆盖索引,而不是获取整个文档。也就是说要指定不需要的字段不返回(包括 _id )。如果在一个含有数组的字段上进行查询,这个索引永远无法进行覆盖查询。

$ 操作符如何使用索引

有一些查询根本无法使用索引,有些查询使用索引会更加高效。

低效的操作符

范围

将精确匹配的字段(比如 “x” : “foo” )放在前面,将用于范围匹配的字段(比如 “y” : {“$gt” : 3, “$lt” : 5} )放在最后。这样查询可以先使用第一个索引键进行精确匹配,再使用第二个索引范围在这个结果集内部进行搜索。在一次查询中使用两个范围通常会导致低效的查询计划。

OR 查询

MongoDB 在查询时只能使用一个索引。但是 $or 是个例外, $or 可以对每个子句都使用索引,因为 $or 实际上是执行两次查询之后将结果集合并。通常来说查询两次的效率没有查询一次那么高,所以尽可能使用 $in 而不是 $or 。如果不得不使用 $or ,记住,MongoDB 需要查询每次的结果集并且从中移除重复的文档。使用 $in 查询时无法控制文档顺序(除非进行排序)。

索引对象与数组

MongoDB 允许深入文档内部,对嵌套字段和数组建立索引。嵌套对象和数组字段可以与复合索引中的顶级字段一起使用,虽然他们比较特殊,但是大多数情况下与“正常”索引字段的表现是一致的。

索引嵌套文档

对潜逃文档本身建立索引和对其某个字段建立索引是不同的。

索引数组

对数组进行索引,可以高效的搜索数组中的特定元素。对数组建立索引实际上是对每个元素建立一个索引条目,而不是数组本身,因此数组索引的代价比单值索引的代价高。每个索引中的数组字段最多只能有一个,这是为了避免在多键索引中索引条目爆炸性增长。

多键索引

对于一个索引,如果在某个文档中是一个数组,那么它会被标记为多键索引,这个行为不可逆转。如果你想让它变成非多键索引,必须保证文档中没有数组,删除新建索引才行。多键索引的引索会比非多键索引慢,因为可能有多个索引指向同一个文档,需要去重后返回。

索引基数

基数就是每个集合中每个字段拥有不同值的数量。通常一个字段的基数越高,这个键上的索引就越有用。这是因为索引能够迅速将搜索范围缩小到一个比较小的结果集。对于基数小的索引,通常无法排除大量的可能匹配。应该在基数比较高的键上建立索引,或者将它们放在前面。

使用 explain() 和 hint()

explain() 能提供大量与查询相关的信息,是重要的诊断工具。分片返回的是多个 explain() 的集合,因为查询会在多个服务器进行。不使用索引的查询的 explain() 是基础 explain() 类型。不使用索引的查询使用的是 BtreeCursor (基础游标)

explain() 显示的字段

  • cursor :BtreeCursor age_1_username_1

这次查询使用什么索引。

  • isMultiKey :false

本次查询是否使用了多键索引

  • n :8332

本次查询的文档数量

  • nscannedObjects :8332

安照索引指针去磁盘查找实际文档的次数

  • nscanned :8332

如果有使用索引,表示查找过的索引条目数量。如果本次是一次全表扫描,它表示检查过的文档数量。

  • scanAndOrder :false

MongoDB 是否在内存中对结果进行了排序

  • indexOnly :false

MongoDB 是否只使用索引就能完成此次查询。

  • nYields :0

为了让写入请求能过顺利进行,本次查询暂停的次数。

  • millis :91

数据库执行本次查询所消耗的毫秒数。

  • indexBounds :{..}

描述索引使用的情况,给出索引的遍历范围。

使用 hint() 可以强制指定查询使用的索引。当查询没有使用你期望的索引时使用,如果 MongoDB 不知道如何使用你指定的索引那么执行效率会降低。

查询优化器

如果一个索引能够精确匹配一个查询,查询优化器会使用这个索引。否则,将会有多个索引都适合你的查询,将会从可能的索引子集中为每次查询计划选择一个,这些查询计划并行执行。最早返回 100 个结果的就是最终计划,其他计划被终止。这个计划被缓存,下一次依旧使用该计划。如果发生了比较大的数据变动,查询优化器会重新评估计划。

何时不应该使用索引

使用索引查询时需要两次查找:
一次是查找索引条目,一次是根据索引指针去查找相应的文档。

索引通常适用的情况 全表扫描通常适用的情况
集合较大 集合较小
文档较大 文档较小
选择性查询 非选择性查询

可以使用 $natural 进行全表扫描。

  1. > db.entries.find({"created_id":{"$lt" : hourAgo}}).hint({"$natural" : 1})

它有个副作用: 返回的结果就是按照磁盘上的顺序排列的。对于一个活跃的数据集合是没有意义的。但是对于一个只要进行插入的工作来说,如果要得到最新的文档,就非常有用了。

索引类型

唯一索引

确保集合的每一个文档的指定键都有唯一值

让用户有不同的名字

  1. > db.users.ensureIndex({"username":1}, {"unique":true})

超过 8K 的键不会受到唯一索引的约束:可以插入多个同样的 8K 长的字符串。

唯一复合索引

单个键的值可以相同,但所有的组合键必须是唯一的

去除重复

当在具有重复值的集合上创建索引时,需要删除重复值,再建立索引。创建索引时使用 dropDups 选项,如果遇到重复值,第一个值会被保留,之后的重复文档将会被删除。

  1. > db.people.ensureIndex({"username":1}, {"unique":true, "dropDups":true})

dropDups 强制性创建唯一索引,你无法控制哪些文档被删除,对重要的数据不要使用它。

稀疏索引

使用 sparse 创建稀疏索引,当字段存在时它必须是唯一的(唯一性是由unique 选项决定的),它可以不存在

  1. > db.ensureIndex({"email":1}, {"unique":true, "sparse":true})

稀疏索引可以不是唯一的,只需要去掉 unique 选项。

索引管理

数据库索引信息都保存在 system.indexes 集合中。它只能通过 ensureIndex 或者 dropIndexes 对其进行操作。
创建一个索引后,可以执行 db.collectionName.getIndexes() 来查看给定集合上的所有索引信息。

参考

[1] MongoDB权威指南