MongoDB(index)

索引(index)

索引 index经常用于常用的查询,如果设计得好,在创建索引之后的查询会有提升效率的效果。但是用之不当的话也可能会没有任何效果,甚至产生反效果,还浪费空间去存储索引信息。因为它事关数据的存储方式,和storage engine相关。
如下是一个使用了index的例子,其中只是创建了一个score的索引,此时如果我们需要查询的field与score有关的话,查询起来可能就会变得快了。注意只是“可能”,这与索引的设置方式有关。“快”是因为引擎中存储了index与对应的文档索引,类似于“array[index]=文档下标”,所以可以通过index直接找到那些在collection中的文档。一般情况下是逐个取出,即在index中取出一个“下标”,然后在存储collection区域索引这个“下标”并取出此文档,因此要考虑效率相关的问题。

index-for-sort.png

支持性

官方文档写道“ MongoDB defines indexes at the collection level and supports indexes on any field or sub-field of the documents in a MongoDB collection. ”关键词是“collection level”和“any field or sub-field”。实现index的数据结构是B-tree。

单索引(single field)

MongoDB支持对任何field的索引,而且默认存在的索引是升序的_id域。此外,MongoDB支持创建除了_id域之外的单个field的文档索引,可以指定为升/降序。如果只是对一个field创建索引,那么升降序都是无所谓的,因为可以从任一边进行遍历index。

多索引(compound index)。

多个field组合的索引也是可以创建的,但是创建索引时所指定field的顺序是很重要的,直接关系到在排序时这个索引是否能提供便捷。比如创建的索引是{ userid: 1, score: -1},意味着索引是根据userid排序的,对于userid相同的才根据score进行内部排序。此时如果你想要排序的规则是{score: 1, userid: 1},那么MongoDB很可能做不到任何优化。因为它只是提供了{ userid: 1, score: -1}{ userid: -1, score: 1}这两种排序的支持。
关于前缀,比如有{ "item": 1, "location": 1, "stock": 1 }这样的一个index,它的前缀有 { item: 1 }{ item: 1, location: 1 }{ "item": 1, "location": 1, "stock": 1 },这些前缀也支持便捷查询提供便捷。其他的field就不支持了。如果想使用{ "item": 1, "stock": 1 }来排序的话,也是可以的,但是总会比单个item或stock要慢。如果同时有{ a: 1, b: 1 }{ a: 1 }两个index存在,其实{ a: 1 }是多余的,因为已经包含在了{ a: 1, b: 1 }里面了。

Dot Notation

在创建索引的时候还有一个“点”的概念,作用是可以建立内嵌文档的索引,这样的索引可以让你根据内嵌文档的相关属性来查找整个collection。“点”的格式就像是scores.score。比如有如下文档:

{
  name: "Tom",
  age: 20,
  scores: {
    {  score: 99,
       class: "history"
    },
    {  ...
    }
    ...
  }
}

然后执行db.students.createIndex({'scores.score':1});就可以成功创建基于内嵌文档属性的索引了。

内嵌field的index(multikey)

如果文档中含有array,可以直接对其名称建立索引,这样MongoDB就会为内嵌数组中的每个元素建立一个独立的索引。比如有内嵌数组arr:[10086,10010],那么创建索引是db.collection.createIndex({"collection.arr": 1})
但是有些索引是不允许创建的。比如一个文档中含有a和b两个array,你可能会这样创建索引{ a: 1, b: 1 } ,不幸的是,这样是不允许的。可能是因为a*b之后所创建的索引可能太大了。如果{ a: 1, b: 1 }的索引已经创建了,则a和b当中必定有一个是非array,此时插入一个a和b都是array的文档就会失败。
类似的,如下的内嵌文档也可以建立索引。比如可以db.test.createIndex( { "stock.size": 1, "stock.quantity": 1 } )

{ 
  _id: 3,
  stock: [ 
    { size: "S", color: "red",  quantity: 25 },
    { size: "S", color: "blue", quantity: 10 }, 
    { size: "M", color: "blue", quantity: 50 } 
  ]
}

建立之后就可以利用于find或者sort了,比如:

db.test.find( ).sort( { "stock.size": 1, "stock.quantity": 1 } )
db.test.find( { "stock.size": "M" } ).sort( { "stock.quantity": 1 } )

有个不同的例子,可以观察一下区别。如下的文档:

{ 
  _id: ObjectId(...), 
  metro: { 
    city: "New York", 
    state: "NY" 
  }, 
  name: "Giant Factory"
}

有两种创建索引的情况需要讨论:
1)直接对metro创建索引db.test.createIndex( { "metro": 1 } ),那么你可以这样正常使用db.test.find( { metro: { city: "New York", state: "NY" } } )来得到index的支持,而db.test.find( { metro: { state: "NY", city: "New York" } } )就不能使用到所创建的index了。其实这样创建的index只有完全匹配了整个内嵌文档时才能发挥作用,这并没有充分发挥index特性。
2)对metro的某个field创建索引,比如执行了db.test.createIndex( { "metro.city": 1 } ),那么使用此index应该是这样的db.test.find( { "metro.city": 1} ),而如果指定了value,比如db.users.find( { "user.login": "tester" } ),这样就不行了。

文本索引(text index)

当前默认版本是version 3。
文本索引,顾名思义就是用于搜索文本的,可以用于搜索所有的value,也可以搜索指定的field对应的value。只要field对应value是string,或者对应的value是array且array中的元素是string,那么文本索引都可以索引该field。

  • 创建test index大概是这样的:
    db.reviews.createIndex( { comments: "text" } )
    或者创建复合索引是这样的:
    db.reviews.createIndex( { subject: "text", comments: "text" } )
    或者对所有"field: string"创建索引(Wildcard text index):
    db.collection.createIndex( { "$**": "text" } )

  • 删除索引仅需要指出该索引名称即可(不能删除_id索引):
    db.pets.dropIndex( "catIdx" )
    或者
    db.pets.dropIndex( { "cat" : -1 } )

  • 如果连索引名称都忘了,那么可以查询该collection设置过的所有索引:
    db.collection.getIndexes()

更高级的操作是,可以指定权值weight,若不指定则默认每个field的weight为1。为每个需要关注的field指定一个合适的weight可以达到这样的效果,对于搜索到的每个文本串,MongoDB都计算出该文档所具有的总权值sum。sum值可以用于控制搜索的结果,具体参考$meta操作符。

默认情况下,text index的名称为所有的field名称用_text连起来,比如db.collection.createIndex( { content: "text", "users.comments": "text", "users.profiles": "text" })的索引名称为content_text_users.comments_text_users.profiles_text。但是当名称太长了的时候,可以这样自定义索引名称db.collection.createIndex( { content: "text", "users.comments": "text", "users.profiles": "text" }, { name: "MyTextIndex" })

哈希索引(hashed index)

好像是用于sharding分片架构的,先mark一下。hashed index可以用来支持匹配查询,但不支持范围的查询,也不支持符合索引。

局限性

使用索引的同时还要注意一些限制,比如索引键的长度,一个集合可以建立多少个索引等等,先讨论一些比较重要的局限。

关于text index,可以搭配普通的index使用,但在使用上还有一些限制,就是只能用来缩小搜索text的范围,也就要求了前面是完全匹配的索引,比如db.inventory.createIndex( { department: 1, description: "text" }),在find中使用的时候就可以这样使用了db.inventory.find( { department: "kitchen", $text: { $search: "green" } } ),效果就是在指定的department中搜索text。一般情况下,假设a是一个复合索引,那么可以这样创建索引db.collection.createIndex( { a: 1, "$**": "text" } ),此时a必须进行完全匹配再进行文本搜索才会被支持。而且,不支持与multi-key或geospatial域搭配。

下面有一些不引人注意的限制:

  • 索引键数量的限制,当创建的索引键超过了这个限制的话,MongoDB不会再创建索引键。
  • 每个collection至多可以创建64个index。
  • 整个索引串<databasename>.<collection name>.$<index name>的长度不得超过128个字符,系统创建的index name一般是由field和 name和index type组成,使用组合index的时候就会比较长了,但index name是可以通过createIndex()方法来指定的。
  • 组合index的个数不能超过31个。
  • 一个集合至多拥有一个text index。
  • 包含搜索text的查询时不能使用hint(),更详细的参考Text Index Restrictions

索引属性(index property)

TTL index

TTL index是一种作用在单个field上的索引,称为索引似乎有点误导人。其他它的作用就是设置文档的存活时长,经过了指定的秒数之后就会自动删除文档。但是这只能针对field类型是date或者是个包含date的数组(按照其中最小的一个来作为基数),其他类型则不会有自动删除的效果。
指定时长的单位是秒,但是MongoDB会在每60秒才执行一次remove操作,所以可能会有这样的情况,你指定了10秒删一次,但是30秒了文档却仍存在,后来又不见了,就是这个原因。remove操作是在后台自动进行的,不会进行任何的提示,也不会报任何执行结果,但可以参考db.currentOp()或database profiler。如果是在replica sets的话,只会删除primary中的文档。
一般是这样创建的TTL index:
db.eventlog.createIndex( { "lastDate": 1 }, { expireAfterSeconds: 3600 } )
注意,不能为_id域和复合索引指定TTL index,同时,MongoDB也不支持将TTL index作用于固定集合(capped collection)。一旦指定了TTL就不能通过createIndex()来修改时长了,也不能为同一个field指定多次TTL,只能是删除后重建。

unique index

只要指定了某个field是唯一的,那么在同一个collection中就不允许存在相同的field值,MongoDB默认创建的unique field就是_id。
unique index一般是这样创建的:
db.members.createIndex( { "user_id": 1 }, { unique: true } )
但是这个唯一性只是在同一集合中的不同文档间有效,也就是说下面的例子并不冲突:

db.collection.createIndex( { "a.b": 1 }, { unique: true } )
db.collection.insert( { a: [ { b: 5 }, { b: 5 } ] } )  //此新文档中的a.b在其他文档不具备即可

如果文档中没有存在unique index field,那么该文档的对应field就为null,这样的文档是可以存在的,但是默认情况下不能够有多个存在,这样就会有多个null,即冲突了(对于复合索引来说,只要组合起来是唯一就不会有冲突。)。这可以通过使用sparse index来解决这个问题。
不能对于已经建立hashed index的field建立unique index。

Partial Indexes(3.2 新增的)

Partial index提供了只索引集合中的部分文档的功能,而不是全部文档。这样做的好处就是,只索引那些我们所关心的文档,比如满足某个条件的文档。在查询中使用的时候就会有些限制了,超出关心文档的范围就不能够利用到这个partial索引。
如下是创建一个包含partial index的复合索引的例子:
db.restaurants.createIndex( { cuisine: 1 }, { partialFilterExpression: { rating: { $gt: 5 } } })
如果查询的范围是在关心的范围之内,那么这个partial index就起作用了,比如:
db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } )
然而,下面的2个例子就使用不到这个partial index了,原因是超出了关心范围 :
db.restaurants.find( { cuisine: "Italian", rating: { $lt: 8 } } )
db.restaurants.find( { cuisine: "Italian" } )

注意:

  • 创建索引时不能同时指定 partialFilterExpression和sparse选项。
  • 不能创建多个仅仅是过滤表达式不同的多个版本的索引。
  • _id索引和shard key都不能是partial index。
  • 前缀的限制,要理解清楚再使用。

Sparse Indexes

稀疏索引只包含那些具有该field的文档(即使是null),其他的文档都会被忽略。如果我们只关心那些具有该field的文档,而这些文档又偏少,那么这样的索引就可以有效率的提升。因为普通的索引对于缺失index field的文档都是默认保存着一个null值。2dsphere (version 2)、2dgeoHaystack、text等索引永远都是sparse index。
可以认为,部分索引partial index是稀疏索引sparse index的超集,即可以用稀疏索引实现的操作都能用是部分索引来实现,官网有清晰的例子。
一般情况下可以这样创建一个sparse index:
db.addresses.createIndex( { "xmpp_id": 1 }, { sparse: true } )
配合hint()来使用这个索引在某种情况下就可以达到提升效率的效果。但是需要注意的是,这种index并不能用于sort功能(有些缺失field的文档使其无法工作)。


创建索引

默认情况下,在创建索引的过程中,正在执行操作的集合不允许被读写,直到操作完成。如果创建的过程比较漫长的话,你又想操作这个集合,那么可以选择在后台执行(不是真的在后台运行,在mongo shell中一旦执行创建索引操作就会被阻塞直到完成,需要另开终端才行),此时可以操作这个集合了。创建这样的索引:
db.people.createIndex( { "name": 1}, {background: true} )
这个选项是可以和其他选项搭配的,比如:
db.people.createIndex( { zipcode: 1}, {background: true, sparse: true } )
在2.4版本之前只能够有一个创建索引的操作在后台运行,现在可以同时运行多个了,但是好像只会有一个是在运作的,而其他都是处于等待队列中。而且后台运行会比前台运行要慢。如果在执行操作的过程中,mongod关闭了,那么在重启mongod之后会在前台重新开始被中断的操作。
只要index在build的过程中遭遇任何的错误,比如重复key错误,则mongod就会出错而退出。如果出错了之后要重启,可以使用storage.indexBuildRetry 或者 --noIndexBuildRetry来跳过重新开始中断的创建过程。
一般情况下,普通的索引名称的构造规则是这样的:
db.products.createIndex( { item: 1, quantity: -1 } )
索引的默认名称为:item_1_quantity_-1。
可以在创建为其指定一个名称:
db.products.createIndex( { item: 1, quantity: -1 } , { name: "inventory" } )


交叉索引(index intersection)

如果想知道find的过程中是否使用了我们创建过的索引,可以使用.explain(),比如下面的例子:
db.orders.find( { item: "abc123", qty: { $gt: 15 } } ).explain()
一般情况下,MongoDB会自动选择合适的索引来支持查询操作(比如匹配前缀,交换查询表达式的顺序),每次都会选择最佳的计划来执行,下次再执行就会按照最佳的方式了,还会不断更新最优计划,一切都很智能。
只要满足如下条件之一就会重新优化最佳计划:
1)一个集合接收到1千次的写操作。
2)使用了reIndex操作。
3)添加/删除一个index。
4)重新启动mongod。

但是,这么智能的东西也可能会达不到我们的特殊要求,此时可以用.hint()来让它按照我们的指示使用某个已存在的索引,这在充分了解其中的机理和利弊时使用可以达到特殊的目的。
看下面的例子就知道了,比如有如下的index:
{ status: 1, ord_date: -1 }
那么如下的查询就支持了:
db.orders.find( { ord_date: { $gt: new Date("2014-02-01") }, status: {$in:[ "P", "A" ] } })
其实这两个条件完全是独立的,交换顺序的结果仍是一样的,只是如果按照上面的索引,查询的时候就按索引中每个条件的顺序来查询了。复合索引中的index顺序也是很重要的,关系到查询时可以缩小的范围大小。
但是上面的索引就不支持如下这两个操作:

db.orders.find( { ord_date: { $gt: new Date("2014-02-01") } } )
db.orders.find( { } ).sort( { ord_date: 1 } )

因为使用index的前提是,符合某个前缀,或者顺序无关时可以使用。可以使用.explain("executionStats")来查看查询的执行信息。测试了类似于上面的例子,如果查询支持索引,那么检索的文档数大大减少,甚至等于选中的文档数。反之,如果不支持,就会将整个集合都检索一遍,这不是我们想看到的。
使用.explain()可以大概关注几个点,比如:

"nReturned" : <int>  选中的文档数量
"executionTimeMillis" : <int>  本次检索所用的时间
"totalKeysExamined" : <int>    检索了多少个索引项
"totalDocsExamined" : <int>  检索了多少个文档

其他的项如果有兴趣可以参考Explain Result


推荐阅读更多精彩内容