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