第5章 索引
本章介绍MongoDB的索引,索引可以用来优化查询,而且在某些特定类型的查询中,索引是必不可少的。
- 什么是索引?为什么要用索引?
- 如何选择需要建立索引的字段?
- 如何强制使用索引?如何评估索引的效率?
- 创建索引和删除索引。
为集合选择合适的索引是提高性能的关键。
5.1 索引简介
数据库索引与书籍的索引类似。有了索引就不需要翻整本书,数据库可以直接在索引中查找,在索引中找到条目以后,就可以直接跳转到目标文档的位置,这能使查找速度提高几个数量级。
不使用索引的查询称为全表扫描(这个术语来自关系型数据库),也就是说,服务器必须查找完一整本书才能找到查询结果。这个处理过程与我们在一本没有索引的书中查找信息很像:从第1页开始一直读完整本书。通常来说,应该尽量避免全表扫描,因为对于大集合来说,全表扫描的效率非常低。
来看一个例子,我们创建了一个拥有1 000 000个文档的集合(如果你想要10 000 000或者100 000 000个文档也行,只要你有那个耐心):
> for (i=0; i<1000000; i++) {
... db.users.insert(
... {
... "i" : i,
... "username" : "user"+i,
... "age" : Math.floor(Math.random()*120),
... "created" : new Date()
... }
... );
... }
如果在这个集合上做查询,可以使用explain()
函数查看MongoDB在执行查询的过程中所做的事情。下面试着查询一个随机的用户名:
> db.users.find({username: "user101"}).explain()
{
"cursor" : "BasicCursor",
"nscanned" : 1000000,
"nscannedObjects" : 1000000,
"n" : 1,
"millis" : 721,
"nYields" : 0,
"nChunkSkips" : 0,
"isMultiKey" : false,
"indexOnly" : false,
"indexBounds" : {
}
}
5.2节会详细介绍输出信息里的这些字段,目前来说可以忽略大多数字段。"nscanned"
是MongoDB在完成这个查询的过程中扫描的文档总数。可以看到,这个集合中的每个文档都被扫描过了。也就是说,为了完成这个查询,MongoDB查看了每一个文档中的每一个字段。这个查询耗费了将近1秒的时间才完成:"millis"
字段显示的是这个查询耗费的毫秒数。
字段"n"
显示了查询结果的数量,这里是1,因为这个集合中确实只有一个username
为"user101"
的文档。注意,由于不知道集合里的username
字段是唯一的,MongoDB不得不查看集合中的每一个文档。为了优化查询,将查询结果限制为1,这样MongoDB在找到一个文档之后就会停止了:
> db.users.find({username: "user101"}).limit(1).explain()
{
"cursor" : "BasicCursor",
"nscanned" : 102,
"nscannedObjects" : 102,
"n" : 1,
"millis" : 2,
"nYields" : 0,
"nChunkSkips" : 0,
"isMultiKey" : false,
"indexOnly" : false,
"indexBounds" : {
}
}
现在,所扫描的文档数量极大地减少了,而且整个查询几乎是瞬间完成的。但是,这个方案是不现实的:如果要查找的是user999999
呢?我们仍然不得不遍历整个集合,而且,随着用户的增加,查询会越来越慢。
对于此类查询,索引是一个非常好的解决方案:索引可以根据给定的字段组织数据,让MongoDB能够非常快地找到目标文档。下面尝试在username
字段上创建一个索引:
> db.users.ensureIndex({"username" : 1})
由于机器性能和集合大小的不同,创建索引有可能需要花几分钟时间。如果对ensureIndex
的调用没能在几秒钟后返回,可以在另一个shell中执行db.currentOp()
或者是检查mongod的日志来查看索引创建的进度。
索引创建完成之后,再次执行最初的查询:
> db.users.find({"username" : "user101"}).explain()
{
"cursor" : "BtreeCursor username_1",
"nscanned" : 1,
"nscannedObjects" : 1,
"n" : 1,
"millis" : 3,
"nYields" : 0,
"nChunkSkips" : 0,
"isMultiKey" : false,
"indexOnly" : false,
"indexBounds" : {
"username" : [
[
"user101",
"user101"
]
]
}
}
这次explain()
的输出内容比之前复杂一些,但是目前我们只需要注意"n"
、"nscanned"
和"millis"
这几个字段,可以忽略其他字段。可以看到,这个查询现在几乎是瞬间完成的(甚至可以更好),而且对于任意username
的查询,所耗费的时间基本一致:
> db.users.find({username: "user999999"}).explain().millis
1
可以看到,使用了索引的查询几乎可以瞬间完成,这是非常激动人心的。然而,使用索引是有代价的:对于添加的每一个索引,每次写操作(插入、更新、删除)都将耗费更多的时间。这是因为,当数据发生变动时,MongoDB不仅要更新文档,还要更新集合上的所有索引。因此,MongoDB限制每个集合上最多只能有64个索引。通常,在一个特定的集合上,不应该拥有两个以上的索引。于是,挑选合适的字段建立索引非常重要。
MongoDB的索引几乎与传统的关系型数据库索引一模一样,所以如果已经掌握了那些技巧,则可以跳过本节的语法说明。后面会介绍一些索引的基础知识,但一定要记住这里涉及的只是冰山一角。绝大多数优化MySQL/Oracle/SQLite索引的技巧同样也适用于MongoDB(包括“Use the Index, Luke”上的教程http://use-the-index-luke.com)。
为了选择合适的键来建立索引,可以查看常用的查询,以及那些需要被优化的查询,从中找出一组常用的键。例如,在上面的例子中,查询是在"username"
上进行的。如果这是一个非常通用的查询,或者这个查询造成了性能瓶颈,那么在"username"
上建立索引会是非常好的选择。然而,如果这只是一个很少用到的查询,或者只是给管理员用的查询(管理员并不需要太在意查询耗费的时间),那就不应该对"username"
建立索引。
5.1.1 复合索引简介
索引的值是按一定顺序排列的,因此,使用索引键对文档进行排序非常快。然而,只有在首先使用索引键进行排序时,索引才有用。例如,在下面的排序里,"username"
上的索引没什么作用:
> db.users.find().sort({"age" : 1, "username" : 1})
这里先根据"age"
排序再根据"username"
排序,所以"username"
在这里发挥的作用并不大。为了优化这个排序,可能需要在"age"
和"username"
上建立索引:
> db.users.ensureIndex({"age" : 1, "username" : 1})
这样就建立了一个复合索引(compound index)。如果查询中有多个排序方向或者查询条件中有多个键,这个索引就会非常有用。复合索引就是一个建立在多个字段上的索引。
假如我们有一个users
集合(如下所示),如果在这个集合上执行一个不排序(称为自然顺序)的查询:
> db.users.find({}, {"_id" : 0, "i" : 0, "created" : 0})
{ "username" : "user0", "age" : 69 }
{ "username" : "user1", "age" : 50 }
{ "username" : "user2", "age" : 88 }
{ "username" : "user3", "age" : 52 }
{ "username" : "user4", "age" : 74 }
{ "username" : "user5", "age" : 104 }
{ "username" : "user6", "age" : 59 }
{ "username" : "user7", "age" : 102 }
{ "username" : "user8", "age" : 94 }
{ "username" : "user9", "age" : 7 }
{ "username" : "user10", "age" : 80 }
...
如果使用{"age" : 1, "username" : 1}
建立索引,这个索引大致会是这个样子:
[0, "user100309"] -> 0x0c965148
[0, "user100334"] -> 0xf51f818e
[0, "user100479"] -> 0x00fd7934
...
[0, "user99985" ] -> 0xd246648f
[1, "user100156"] -> 0xf78d5bdd
[1, "user100187"] -> 0x68ab28bd
[1, "user100192"] -> 0x5c7fb621
...
[1, "user999920"] -> 0x67ded4b7
[2, "user100141"] -> 0x3996dd46
[2, "user100149"] -> 0xfce68412
[2, "user100223"] -> 0x91106e23
...
每一个索引条目都包含一个"age"
字段和一个"username"
字段,并且指向文档在磁盘上的存储位置(这里使用十六进制数字表示,可以忽略)。注意,这里的"age"
字段是严格升序排列的,"age"
相同的条目按照"username"
升序排列。每个"age"
都有大约8000个对应的"username"
,这里只是挑选了少量数据用于传达大概的信息。
MongoDB对这个索引的使用方式取决于查询的类型。下面是三种主要的方式。
db.users.find({"age" : 21}).sort({"username" : -1})
这是一个点查询(point query),用于查找单个值(尽管包含这个值的文档可能有多个)。由于索引中的第二个字段,查询结果已经是有序的了:MongoDB可以从{"age" : 21}匹配的最后一个索引开始,逆序依次遍历索引:
[21, "user999977"] -> 0x9b3160cf
[21, "user999954"] -> 0xfe039231
[21, "user999902"] -> 0x719996aa
...
这种类型的查询是非常高效的:MongoDB能够直接定位到正确的年龄,而且不需要对结果进行排序(因为只需要对数据进行逆序遍历就可以得到正确的顺序了)。
注意,排序方向并不重要:MongoDB可以在任意方向上对索引进行遍历。
db.users.find({"age" : {"$gte" : 21, "$lte" : 30}})
这是一个多值查询(multi-value query),查找到多个值相匹配的文档(在本例中,年龄必须介于21到30之间)。MongoDB会使用索引中的第一个键"age"
得到匹配的文档,如下所示:
[21, "user100000"] -> 0x37555a81
[21, "user100069"] -> 0x6951d16f
[21, "user1001"] -> 0x9a1f5e0c
[21, "user100253"] -> 0xd54bd959
[21, "user100409"] -> 0x824fef6c
[21, "user100469"] -> 0x5fba778b
...
[30, "user999775"] -> 0x45182d8c
[30, "user999850"] -> 0x1df279e9
[30, "user999936"] -> 0x525caa57
通常来说,如果MongoDB使用索引进行查询,那么查询结果文档通常是按照索引顺序排列的。
db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).sort({"username":1})
这是一个多值查询,与上一个类似,只是这次需要对查询结果进行排序。跟之前一样,MongoDB会使用索引来匹配查询条件:
[21, "user100000"] -> 0x37555a81
[21, "user100069"] -> 0x6951d16f
[21, "user1001"] -> 0x9a1f5e0c
[21, "user100253"] -> 0xd54bd959
...
[22, "user100004"] -> 0x81e862c5
[22, "user100328"] -> 0x83376384
[22, "user100335"] -> 0x55932943
[22, "user100405"] -> 0x20e7e664
...
然而,使用这个索引得到的结果集中"username"
是无序的,而查询要求结果以"username"
升序排列,所以MongoDB需要先在内存中对结果进行排序,然后才能返回。因此,这个查询通常不如上一个高效。
当然,查询速度取决于有多少个文档与查询条件匹配:如果结果集中只有少数几个文档,MongoDB对这些文档进行排序并不需要耗费多少时间。如果结果集中的文档数量比较多,查询速度就会比较慢,甚至根本不能用:如果结果集的大小超过32 MB,MongoDB就会出错,拒绝对如此多的数据进行排序:
Mon Oct 29 16:25:26 uncaught exception: error: {
"$err" : "too much data for sort() with no index. add an index or
specify a smaller limit",
"code" : 10128
}
最后一个例子中,还可以使用另一个索引(同样的键,但是顺序调换了):{"username" : 1, "age" : 1}
。MongoDB会反转所有的索引条目,但是会以你期望的顺序返回。MongoDB会根据索引中的"age"
部分挑选出匹配的文档:
["user0", 69]
["user1", 50]
["user10", 80]
["user100", 48]
["user1000", 111]
["user10000", 98]
["user100000", 21] -> 0x73f0b48d
["user100001", 60]
["user100002", 82]
["user100003", 27] -> 0x0078f55f
["user100004", 22] -> 0x5f0d3088
["user100005", 95]
...
这样非常好,因为不需要在内存中对大量数据进行排序。但是,MongoDB不得不扫描整个索引以便找到所有匹配的文档。因此,如果对查询结果的范围做了限制,那么MongoDB在几次匹配之后就可以不再扫描索引,在这种情况下,将排序键放在第一位是一个非常好的策略。
可以通过explain()
来查看MongoDB对db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).sort({"username" : 1})
的默认行为:
> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).
... sort({"username" : 1}).
... explain()
{
"cursor" : "BtreeCursor age_1_username_1",
"isMultiKey" : false,
"n" : 83484,
"nscannedObjects" : 83484,
"nscanned" : 83484,
"nscannedObjectsAllPlans" : 83484,
"nscannedAllPlans" : 83484,
"scanAndOrder" : true,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 2766,
"indexBounds" : {
"age" : [
[
21,
30
]
],
"username" : [
[
{
"$minElement" : 1
},
{
"$maxElement" : 1
}
]
]
},
"server" : "spock:27017"
}
可以忽略大部分字段,后面会有相关介绍。注意,"cursor"
字段说明这次查询使用的索引是 {"age" : 1, "user name" : 1}
,而且只查找了不到1/10的文档("nscanned"
只有83484),但是这个查询耗费了差不多3秒的时间("millis"
字段显示的是毫秒数)。这里的"scanAndOrder"
字段的值是true
:说明MongoDB必须在内存中对数据进行排序,如之前所述。
可以通过hint
来强制MongoDB使用某个特定的索引,再次执行这个查询,但是这次使用{"username" : 1, "age" : 1}
作为索引。这个查询扫描的文档比较多,但是不需要在内存中对数据排序:
> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).
... sort({"username" : 1}).
... hint({"username" : 1, "age" : 1}).
... explain()
{
"cursor" : "BtreeCursor username_1_age_1",
"isMultiKey" : false,
"n" : 83484,
"nscannedObjects" : 83484,
"nscanned" : 984434,
"nscannedObjectsAllPlans" : 83484,
"nscannedAllPlans" : 984434,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 14820,
"indexBounds" : {
"username" : [
[
{
"$minElement" : 1
},
{
"$maxElement" : 1
}
]
],
"age" : [
[
21,
30
]
]
},
"server" : "spock:27017"
}
注意,这次查询耗费了将近15秒才完成。对比鲜明,第一个索引速度更快。然而,如果限制每次查询的结果数量,新的赢家产生了:
> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).
... sort({"username" : 1}).
... limit(1000).
... hint({"age" : 1, "username" : 1}).
... explain()['millis']
2031
> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).
... sort({"username" : 1}).
... limit(1000).
... hint({"username" : 1, "age" : 1}).
... explain()['millis']
181
第一个查询耗费的时间仍然介于2秒到3秒之间,但是第二个查询只用了不到1/5秒!因此,应该就在应用程序使用的查询上执行explain()
。排除掉那些可能会导致explain()
输出信息不准确的选项。
在实际的应用程序中,{"sortKey" : 1, "queryCriteria" : 1}
索引通常是很有用的,因为大多数应用程序在一次查询中只需要得到查询结果最前面的少数结果,而不是所有可能的结果。而且,由于索引在内部的组织形式,这种方式非常易于扩展。索引本质上是树,最小的值在最左边的叶子上,最大的值在最右边的叶子上。如果有一个日期类型的"sortKey"
(或是其他能够随时间增加的值),当从左向右遍历这棵树时,你实际上也花费了时间。因此,如果应用程序需要使用最近数据的机会多于较老的数据,那么MongoDB只需在内存中保留这棵树最右侧的分支(最近的数据),而不必将整棵树留在内存中。类似这样的索引是右平衡的(right balanced),应该尽可能让索引是右平衡的。"_id"
索引就是一个典型的右平衡索引。
5.1.2 使用复合索引
在多个键上建立的索引就是复合索引,在上面的小节中,已经使用过复合索引。复合索引比单键索引要复杂一些,但是也更强大。本节会更深入地介绍复合索引。
1. 选择键的方向
到目前为止,我们的所有索引都是升序的(或者是从最小到最大)。但是,如果需要在两个(或者更多)查询条件上进行排序,可能需要让索引键的方向不同。例如,假设我们要根据年龄从小到大,用户名从Z到A对上面的集合进行排序。对于这个问题,之前的索引变得不再高效:每一个年龄分组内都是按照"username"
升序排列的,是A到Z,不是Z到A。对于按"age"
升序排列按"username"
降序排列这样的需求来说,用上面的索引得到的数据的顺序没什么用。
为了在不同方向上优化这个复合排序,需要使用与方向相匹配的索引。在这个例子中,可以使用{"age" : 1, "username" : -1}
,它会以下面的方式组织数据:
[21, "user999977"] -> 0xe57bf737
[21, "user999954"] -> 0x8bffa512
[21, "user999902"] -> 0x9e1447d1
[21, "user999900"] -> 0x3a6a8426
[21, "user999874"] -> 0xc353ee06
...
[30, "user999936"] -> 0x7f39a81a
[30, "user999850"] -> 0xa979e136
[30, "user999775"] -> 0x5de6b77a
...
[30, "user100324"] -> 0xe14f8e4d
[30, "user100140"] -> 0x0f34d446
[30, "user100050"] -> 0x223c35b1
年龄按照从年轻到年长顺序排列,在每一个年龄分组中,用户名是从Z到A排列的(对于我们的用户名来说,也可以说是按照"9"
到"0"
排列的)。
如果应用程序同时需要按照{"age" : 1, "username" : 1}
优化排序,我们还需要创建一个这个方向上的索引。至于索引使用的方向,与排序方向相同就可以了。注意,相互反转(在每个方向都乘以-1)的索引是等价的:{"age" : 1, "user name" : -1}
适用的查询与{"age" : -1, "username" : 1}
是完全一样的。
只有基于多个查询条件进行排序时,索引方向才是比较重要的。如果只是基于单一键进行排序,MongoDB可以简单地从相反方向读取索引。例如,如果有一个基于{"age" : -1}
的排序和一个基于{"age" : 1}
的索引,MongoDB会在使用索引时进行优化,就如同存在一个{"age" : -1}
索引一样(所以不要创建两个这样的索引!)。只有在基于多键排序时,方向才变得重要。
2. 使用覆盖索引(covered index)
在上面的例子中,查询只是用来查找正确的文档,然后按照指示获取实际的文档。然后,如果你的查询只需要查找索引中包含的字段,那就根本没必要获取实际的文档。当一个索引包含用户请求的所有字段,可以认为这个索引覆盖了本次查询。在实际中,应该优先使用覆盖索引,而不是去获取实际的文档。这样可以保证工作集比较小,尤其与右平衡索引一起使用时。
为了确保查询只使用索引就可以完成,应该使用投射(详见4.1.1节)来指定不要返回"_id"
字段(除非它是索引的一部分)。可能还需要对不需要查询的字段做索引,因此需要在编写时就在所需的查询速度和这种方式带来的开销之间做好权衡。
如果在覆盖索引上执行explain()
,"indexOnly"
字段的值要为true
。
如果在一个含有数组的字段上做索引,这个索引永远也无法覆盖查询(因为数组是被保存在索引中的,5.1.4节会深入介绍)。即便将数组字段从需要返回的字段中剔除,这样的索引仍然无法覆盖查询。
3. 隐式索引
复合索引具有双重功能,而且对不同的查询可以表现为不同的索引。如果有一个{"age" : 1, "username" : 1}
索引,"age"
字段会被自动排序,就好像有一个{"age" : 1}
索引一样。因此,这个复合索引可以当作{"age" : 1}
索引一样使用。
这个可以根据需要推广到尽可能多的键:如果有一个拥有N
个键的索引,那么你同时“免费”得到了所有这N
个键的前缀组成的索引。举例来说,如果有一个{"a": 1, "b": 1, "c": 1, …, "z": 1}
索引,那么,实际上我们也可以使用 {"a": 1}
、{"a": 1, "b" : 1}
、{"a": 1, "b": 1, "c": 1}
等一系列索引。
注意,这些键的任意子集所组成的索引并不一定可用。例如,使用{"b": 1}
或者{"a": 1, "c": 1}
作为索引的查询是不会被优化的:只有能够使用索引前缀的查询才能从中受益。
5.1.3 $操作符如何使用索引
有一些查询完全无法使用索引,也有一些查询能够比其他查询更高效地使用索引。本节讲述MongoDB对各种不同查询操作符的处理。
1. 低效率的操作符
有一些查询完全无法使用索引,比如"$where"
查询和检查一个键是否存在的查询({"key" : {"$exists" : true}}
)。也有其他一些操作不能高效地使用索引。
如果"x"
上有一个索引,查询那些不包含"x"
键的文档可以使用这样的索引({"x" : {"$exists" : false}}
。然而,在索引中,不存在的字段和null
字段的存储方式是一样的,查询必须遍历每一个文档检查这个值是否真的为null
还是根本不存在。如果使用稀疏索引(sparse index),就不能使用{"$exists" : true}
,也不能使用{"$exists" : false}
。
通常来说,取反的效率是比较低的。"$ne"
查询可以使用索引,但并不是很有效。因为必须要查看所有的索引条目,而不只是"$ne"
指定的条目,不得不扫描整个索引。例如,这样的查询遍历的索引范围如下:
> db.example.find({"i" : {"$ne" : 3}}).explain()
{
"cursor" : "BtreeCursor i_1 multi",
...,
"indexBounds" : {
"i" : [
[
{
"$minElement" : 1
},
3
],
[
3,
{
"$maxElement" : 1
}
]
]
},
...
}
这个查询查找了所有小于3和大于3的索引条目。如果索引中值为3的条目非常多,那么这个查询的效率是很不错的,否则的话,这个查询就不得不检查几乎所有的索引条目。
"$not"
有时能够使用索引,但是通常它并不知道要如何使用索引。它能够对基本的范围(比如将{"key" : {"$lt" : 7}}
变成 {"key" : {"$gte" : 7}}
)和正则表达式进行反转。然而,大多数使用"$not"
的查询都会退化为进行全表扫描。"$nin"
就总是进行全表扫描。
如果需要快速执行一个这些类型的查询,可以试着找到另一个能够使用索引的语句,将其添加到查询中,这样就可以在MongoDB进行无索引匹配(non-indexed matching)时先将结果集的文档数量减到一个比较小的水平。
假如我们要找出所有没有"birthday"
字段的用户。如果我们知道从3月20开始,程序会为每一个新用户添加生日字段,那么就可以只查询3月20之前创建的用户:
> db.users.find({"birthday" : {"$exists" : false}, "_id" : {"$lt" : march20Id}})
这个查询中的字段顺序无关紧要,MongoDB会自动找出可以使用索引的字段,而无视查询中的字段顺序。
2. 范围
复合索引使MongoDB能够高效地执行拥有多个语句的查询。设计基于多个字段的索引时,应该将会用于精确匹配的字段(比如 "x" : "foo"
)放在索引的前面,将用于范围匹配的字段(比如"y" : {"$gt" : 3, "$lt" : 5}
)放在最后。这样,查询就可以先使用第一个索引键进行精确匹配,然后再使用第二个索引范围在这个结果集内部进行搜索。假设要使用{"age" : 1, "username" : 1}
索引查询特定年龄和用户名范围内的文档,可以精确指定索引边界值:
> db.users.find({"age" : 47,
... "username" : {"$gt" : "user5", "$lt" : "user8"}}).explain()
{
"cursor" : "BtreeCursor age_1_username_1",
"n" : 2788,
"nscanned" : 2788,
...,
"indexBounds" : {
"age" : [
[
47,
47
]
],
"username" : [
[
"user5",
"user8"
]
]
},
...
}
这个查询会直接定位到"age"
为47的索引条目,然后在其中搜索用户名介于"user5"
和"user8"
的条目。
反过来,假如使用{"username" : 1, "age" : 1}
索引,这样就改变了查询计划(query plan),查询必须先找到介于"user5"
和"user8"
之间的所有用户,然后再从中挑选"age"
等于47的用户。
> db.users.find({"age" : 47,
... "username" : {"$gt" : "user5", "$lt" : "user8"}}).explain()
{
"cursor" : "BtreeCursor username_1_age_1",
"n" : 2788,
"nscanned" : 319499,
...,
"indexBounds" : {
"username" : [
[
"user5",
"user8"
]
],
"age" : [
[
47,
47
]
]
},
"server" : "spock:27017"
}
本次查询中MongoDB扫描的索引条目数量是前一个查询的10倍!在一次查询中使用两个范围通常会导致低效的查询计划。
3. OR查询
写作本书时,MongoDB在一次查询中只能使用一个索引。如果你在{"x" : 1}
上有一个索引,在{"y" : 1}
上也有一个索引,在{"x" : 123, "y" : 456}
上进行查询时,MongoDB会使用其中的一个索引,而不是两个一起用。"$or"
是个例外,"$or"
可以对每个子句都使用索引,因为"$or"
实际上是执行两次查询然后将结果集合并。
> db.foo.find({"$or" : [{"x" : 123}, {"y" : 456}]}).explain()
{
"clauses" : [
{
"cursor" : "BtreeCursor x_1",
"isMultiKey" : false,
"n" : 1,
"nscannedObjects" : 1,
"nscanned" : 1,
"nscannedObjectsAllPlans" : 1,
"nscannedAllPlans" : 1,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0,
"indexBounds" : {
"x" : [
[
123,
123
]
]
}
},
{
"cursor" : "BtreeCursor y_1",
"isMultiKey" : false,
"n" : 1,
"nscannedObjects" : 1,
"nscanned" : 1,
"nscannedObjectsAllPlans" : 1,
"nscannedAllPlans" : 1,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0,
"indexBounds" : {
"y" : [
[
456,
456
]
]
}
}
],
"n" : 2,
"nscannedObjects" : 2,
"nscanned" : 2,
"nscannedObjectsAllPlans" : 2,
"nscannedAllPlans" : 2,
"millis" : 0,
"server" : "spock:27017"
}
可以看到,这次的explain()
输出结果由两次独立的查询组成。通常来说,执行两次查询再将结果合并的效率不如单次查询高,因此,应该尽可能使用"$in"
而不是"$or"
。
如果不得不使用"$or"
,记住,MongoDB需要检查每次查询的结果集并且从中移除重复的文档(有些文档可能会被多个"$or"
子句匹配到)。
使用"$in"
查询时无法控制返回文档的顺序(除非进行排序)。例如,使用{"x" : [1, 2, 3]}
与使用{"x" : [3, 2, 1]}
得到的文档顺序是相同的。
5.1.4 索引对象和数组
MongoDB允许深入文档内部,对嵌套字段和数组建立索引。嵌套对象和数组字段可以与复合索引中的顶级字段一起使用,虽然它们比较特殊,但是大多数情况下与“正常”索引字段的行为是一致的。
1. 索引嵌套文档
可以在嵌套文档的键上建立索引,方式与正常的键一样。如果有这样一个集合,其中的第一个文档表示一个用户,可能需要使用嵌套文档来表示每个用户的位置:
{
"username" : "sid",
"loc" : {
"ip" : "1.2.3.4",
"city" : "Springfield",
"state" : "NY"
}
}
需要在"loc"
的某一个子字段(比如"loc.city"
)上建立索引,以便提高这个字段的查询速度:
> db.users.ensureIndex({"loc.city" : 1})
可以用这种方式对任意深层次的字段建立索引,比如你可以在"x.y.z.w.a.b.c"
上建立索引。
注意,对嵌套文档本身("loc"
)建立索引,与对嵌套文档的某个字段("loc.city"
)建立索引是不同的。对整个子文档建立索引,只会提高整个子文档的查询速度。在上面的例子中,只有在进行与子文档字段顺序完全匹配的子文档查询时(比如db.users.find({"loc" : {"ip" : "123.456.789.000", "city" : "Shelbyville", "state" : "NY"}}})
),查询优化器才会使用"loc"
上的索引。无法对形如db.users.find({"loc.city" : "Shelbyville"})
的查询使用索引。
2. 索引数组
也可以对数组建立索引,这样就可以高效地搜索数组中的特定元素。
假如有一个博客文章的集合,其中每个文档表示一篇文章。每篇文章都有一个"comments"
字段,这是一个数组,其中每个元素都是一个评论子文档。如果想要找出最近被评论次数最多的博客文章,可以在博客文章集合中嵌套的"comments"
数组的"date"
键上建立索引:
> db.blog.ensureIndex({"comments.date" : 1})
对数组建立索引,实际上是对数组的每一个元素建立一个索引条目,所以如果一篇文章有20条评论,那么它就拥有20个索引条目。因此数组索引的代价比单值索引高:对于单次插入、更新或者删除,每一个数组条目可能都需要更新(可能有上千个索引条目)。
与上一节中"loc"
的例子不同,无法将整个数组作为一个实体建立索引:对数组建立索引,实际上是对数组中的每个元素建立索引,而不是对数组本身建立索引。
在数组上建立的索引并不包含任何位置信息:无法使用数组索引查找特定位置的数组元素,比如"comments.4"
。
少数特殊情况下,可以对某个特定的数组条目进行索引,比如:
> db.blog.ensureIndex({"comments.10.votes": 1})
然而,只有在精确匹配第11个数组元素时这个索引才有用(数组下标从0开始)。
一个索引中的数组字段最多只能有一个。这是为了避免在多键索引中索引条目爆炸性增长:每一对可能的元素都要被索引,这样导致每个文档拥有n*m个索引条目。假如有一个{"x" : 1, "y" : 1}
上的索引:
> // x是一个数组—— 这是合法的
> db.multi.insert({"x" : [1, 2, 3], "y" : 1})
>
> // y是一个数组——这也是合法的
> db.multi.insert({"x" : 1, "y" : [4, 5, 6]})
>
> // x和y都是数组——这是非法的!
> db.multi.insert({"x" : [1, 2, 3], "y" : [4, 5, 6]})
cannot index parallel arrays [y] [x]
如果MongoDB要为上面的最后一个例子创建索引,它必须要创建这么多索引条目:{"x" : 1, "y" : 4}
、{"x" : 1, "y" : 5}
、{"x" : 1, "y" : 6}
、{"x" : 2, "y" : 4}
、{"x" : 2, "y" : 5}
,{"x" : 2, "y" : 6}
、{"x" : 3, "y" : 4}
、{"x" : 3, "y" : 5}
和{"x" : 3, "y" : 6}
。尽管这些数组只有3个元素。
3. 多键索引
对于某个索引的键,如果这个键在某个文档中是一个数组,那么这个索引就会被标记为多键索引(multikey index)。可以从explain()
的输出中看到一个索引是否为多键索引:如果使用了多键索引,"isMultikey"
字段的值会是true
。索引只要被标记为多键索引,就无法再变成非多键索引了,即使这个字段为数组的所有文档都从集合中删除。要将多键索引恢复为非多键索引,唯一的方法就是删除再重建这个索引。
多键索引可能会比非多键索引慢一些。可能会有多个索引条目指向同一个文档,因此MongoDB在返回结果集时必须要先去除重复的内容。
5.1.5 索引基数
基数(cardinality)就是集合中某个字段拥有不同值的数量。有一些字段,比如"gender"
或者"newsletter opt-out"
,可能只拥有两个可能的值,这种键的基数就是非常低的。另外一些字段,比如"username"
或者"email"
,可能集合中的每个文档都拥有一个不同的值,这类键的基数是非常高的。当然也有一些介于两者之间的字段,比如"age"
或者"zip code"
。
通常,一个字段的基数越高,这个键上的索引就越有用。这是因为索引能够迅速将搜索范围缩小到一个比较小的结果集。对于低基数的字段,索引通常无法排除掉大量可能的匹配。
假设我们在"gender"
上有一个索引,需要查找名为Susan的女性用户。通过这个索引,只能将搜索空间缩小到大约50%,然后要在每个单独的文档中查找"name"
为"Susan"
的用户。反过来,如果在"name"
上建立索引,就能立即将结果集缩小到名为"Susan"
的用户,这样的结果集非常小,然后就可以根据性别从中迅速地找到匹配的文档了。
一般说来,应该在基数比较高的键上建立索引,或者至少应该把基数较高的键放在复合索引的前面(低基数的键之前)。
5.2 使用explain()和hint()
从上面的内容可以看出,explain()
能够提供大量与查询相关的信息。对于速度比较慢的查询来说,这是最重要的诊断工具之一。通过查看一个查询的explain()
输出信息,可以知道查询使用了哪个索引,以及是如何使用的。对于任意查询,都可以在最后添加一个explain()
调用(与调用sort()
或者limit()
一样,不过explain()
必须放在最后)。
最常见的explain()
输出有两种类型:使用索引的查询和没有使用索引的查询。对于特殊类型的索引,生成的查询计划可能会有些许不同,但是大部分字段都是相似的。另外,分片返回的是多个explain()
的聚合(第13章会介绍),因为查询会在多个服务器上执行。
不使用索引的查询的exlpain()
是最基本的explain()
类型。如果一个查询不使用索引,是因为它使用了"BasicCursor"
(基本游标)。反过来说,大部分使用索引的查询使用的是BtreeCursor
(某些特殊类型的索引,比如地理空间索引,使用的是它们自己类型的游标)。
对于使用了复合索引的查询,最简单情况下的explain()
输出如下所示:
> db.users.find({"age" : 42}).explain()
{
"cursor" : "BtreeCursor age_1_username_1",
"isMultiKey" : false,
"n" : 8332,
"nscannedObjects" : 8332,
"nscanned" : 8332,
"nscannedObjectsAllPlans" : 8332,
"nscannedAllPlans" : 8332,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 91,
"indexBounds" : {
"age" : [
[
42,
42
]
],
"username" : [
[
{
"$minElement" : 1
},
{
"$maxElement" : 1
}
]
]
},
"server" : "ubuntu:27017"
}
从输出信息中可以看到它使用的索引是age_1_username_1
。"millis"
表明了这个查询的执行速度,时间是从服务器收到请求开始一直到发出响应为止。然而,这个数值不一定真的是你希望看到的值。如果MongoDB尝试了多个查询计划,那么"millis"
显示的是这些查询计划花费的总时间,而不是最优查询计划所花的时间。
接下来是实际返回的文档数量:"n"
。它无法反映出MongoDB在执行这个查询的过程中所做的工作:搜索了多少索引条目和文档。索引条目是使用"nscanned"
描述的。"nscannedObjects"
字段的值就是所扫描的文档数量。最后,如果要对结果集进行排序,而MongoDB无法对排序使用索引,那么"scanAndOrder"
的值就会是true
。也就是说,MongoDB不得不在内存中对结果进行排序,这是非常慢的,而且结果集的数量要比较小。
现在你已经知道这些基础知识了,接下来依次详细介绍这些字段。
"cursor" : "BtreeCursor age_1_username_1"
BtreeCursor
表示本次查询使用了索引,具体来说,是使用了"age"
和"username"
上的索引{"age" : 1, "username" : 1}
。如果查询要对结果进行逆序遍历,或者是使用了多键索引,就可以在这个字段中看到"reverse"
和"multi"
这样的值。
"isMultiKey" : false
用于说明本次查询是否使用了多键索引(详见5.1.4节)。
"n" : 8332
本次查询返回的文档数量。
"nscannedObjects" : 8332
这是MongoDB按照索引指针去磁盘上查找实际文档的次数。如果查询包含的查询条件不是索引的一部分,或者说要求返回不在索引内的字段,MongoDB就必须依次查找每个索引条目指向的文档。
"nscanned" : 8332
如果有使用索引,那么这个数字就是查找过的索引条目数量。如果本次查询是一次全表扫描,那么这个数字就表示检查过的文档数量。
"scanAndOrder" : false
MongoDB是否在内存中对结果集进行了排序。
"indexOnly" : false
MongoDB是否只使用索引就能完成此次查询(详见“覆盖索引”部分)。
在本例中,MongoDB只使用索引就找到了全部的匹配文档,从"nscanned"
和"n"
相等就可以看出来。然而,本次查询要求返回匹配文档中的所有字段,而索引只包含"age"
和"username"
两个字段。如果将本次查询修改为({"_id" : 0, "age" : 1, "username" : 1}
),那么本次查询就可以被索引覆盖了,"indexOnly"
的值就会是true
。
"nYields" : 0
为了让写入请求能够顺利执行,本次查询暂停的次数。如果有写入请求需要处理,查询会周期性地释放它们的锁,以便写入能够顺利执行。然而,在本次查询中,没有写入请求,因为查询没有暂停过。
"millis" : 91
数据库执行本次查询所耗费的毫秒数。这个数字越小,说明查询效率越高。
"indexBounds" : {…}
这个字段描述了索引的使用情况,给出了索引的遍历范围。由于查询中的第一个语句是精确匹配,因此索引只需要查找42这个值就可以了。本次查询没有指定第二个索引键,因此这个索引键上没有限制,数据库会在"age"
为42的条目中将用户名介于负无穷("$minElement" : 1)
和正无穷("$maxElement" : 1
)的条目都找出来。
再来看一个稍微复杂点的例子:假如有一个{"user name" : 1, "age" : 1}
上的索引和一个 {"age" : 1, "username" : 1}
上的索引。同时查询"username"
和"age"
时,会发生什么情况?呃,这取决于具体的查询:
> db.c.find({age : {$gt : 10}, username : "sally"}).explain()
{
"cursor" : "BtreeCursor username_1_age_1",
"indexBounds" : [
[
{
"username" : "sally",
"age" : 10
},
{
"username" : "sally",
"age" : 1.7976931348623157e+308
}
]
],
"nscanned" : 13,
"nscannedObjects" : 13,
"n" : 13,
"millis" : 5
}
由于在要在"username"
上执行精确匹配,在"age"
上进行范围查询,因此,数据库选择使用{"username" : 1, "age" : 1}
索引,这与查询语句的顺序相反。另一方面来说,如果需要对"age"
精确匹配而对"username"
进行范围查询,MongoDB就会使用另一个索引:
> db.c.find({"age" : 14, "username" : /.*/}).explain()
{
"cursor" : "BtreeCursor age_1_username_1 multi",
"indexBounds" : [
[
{
"age" : 14,
"username" : ""
},
{
"age" : 14,
"username" : {
}
}
],
[
{
"age" : 14,
"username" : /.*/
},
{
"age" : 14,
"username" : /.*/
}
]
],
"nscanned" : 2,
"nscannedObjects" : 2,
"n" : 2,
"millis" : 2
}
如果发现MongoDB使用的索引与自己希望它使用的索引不一致,可以使用hit()
强制MongoDB使用特定的索引。例如,如果希望MongoDB在上个例子的查询中使用{"username" : 1, "age" : 1}
索引,可以这么做:
> db.c.find({"age" : 14, "username" : /.*/}).hint({"username" : 1, "age" : 1})
如果查询没有使用你希望它使用的索引,于是你使用hint
强制MongoDB使用某个索引,那么应该在应用程序部署之前在所指定的索引上执行explain()
。如果强制MongoDB在某个查询上使用索引,而这个查询不知道如何使用这个索引,这样会导致查询效率降低,还不如不使用索引来得快。
查询优化器
MongoDB的查询优化器与其他数据库稍有不同。基本来说,如果一个索引能够精确匹配一个查询(要查询"x"
,刚好在"x"
上有一个索引),那么查询优化器就会使用这个索引。不然的话,可能会有几个索引都适合你的查询。MongoDB会从这些可能的索引子集中为每次查询计划选择一个,这些查询计划是并行执行的。最早返回100个结果的就是胜者,其他的查询计划就会被中止。
这个查询计划会被缓存,这个查询接下来都会使用它,直到集合数据发生了比较大的变动。如果在最初的计划评估之后集合发生了比较大的数据变动,查询优化器就会重新挑选可行的查询计划。建立索引时,或者是每执行1000次查询之后,查询优化器都会重新评估查询计划。
explain()
输出信息里的"allPlans"
字段显示了本次查询尝试过的每个查询计划。
5.3 何时不应该使用索引
提取较小的子数据集时,索引非常高效。也有一些查询不使用索引会更快。结果集在原集合中所占的比例越大,索引的速度就越慢,因为使用索引需要进行两次查找:一次是查找索引条目,一次是根据索引指针去查找相应的文档。而全表扫描只需要进行一次查找:查找文档。在最坏的情况下(返回集合内的所有文档),使用索引进行的查找次数会是全表扫描的两倍,效率会明显比全表扫描低很多。
可惜,并没有一个严格的规则可以告诉我们,如何根据数据大小、索引大小、文档大小以及结果集的平均大小来判断什么时候索引很有用,什么时候索引会降低查询速度(如表5-1所示)。一般来说,如果查询需要返回集合内30%的文档(或者更多),那就应该对索引和全表扫描的速度进行比较。然而,这个数字可能会在2%~60%之间变动。
表5-1 影响索引效率的属性
索引通常适用的情况 | 全表扫描通常适用的情况 |
---|---|
集合较大 | 集合较小 |
文档较大 | 文档较小 |
选择性查询 | 非选择性查询 |
假如我们有一个收集统计信息的分析系统。应用程序要根据给定账户去系统中查询所有文档,根据从初始一直到一小时之前的数据生成图表:
> db.entries.find({"created_at" : {"$lt" : hourAgo}})
我们在"created_at"
上创建索引以提高查询速度。
最初运行时,结果集非常小,可以立即返回。几个星期过去以后,数据开始多起来了,一个月之后,这个查询耗费的时间越来越长。
对于大部分应用程序来说,这很可能就是那个“错误的”查询:真的需要在查询中返回数据集中的大部分内容吗?大部分应用程序(尤其是拥有非常大的数据集的应用程序)都不需要。然而,也有一些合理的情况,可能需要得到大部分或者全部的数据:也许需要将这些数据导出到报表系统,或者是放在批量任务中。在这些情况下,应该尽可能快地返回数据集中的内容。
可以用{"$natural" : 1}
强制数据库做全表扫描。6.1节会介绍$natural
,它可以指定文档按照磁盘上的顺序排列。特别地,$natural
可以强制MongoDB做全表扫描:
> db.entries.find({"created_at" : {"$lt" : hourAgo}}).hint({"$natural" : 1})
使用"$natural"
排序有一个副作用:返回的结果是按照磁盘上的顺序排列的。对于一个活跃的集合来说,这是没有意义的:随着文档体积的增加或者缩小,文档会在磁盘上进行移动,新的文档会被写入到这些文档留下的空白位置。但是,对于只需要进行插入的工作来说,如果要得到最新的(或者最早的)文档,使用$natural
就非常有用了。
5.4 索引类型
创建索引时可以指定一些选项,使用不同选项建立的索引会有不同的行为。接下来的小节会介绍常见的索引变种,更高级的索引类型和特殊选项会在下一章介绍。
5.4.1 唯一索引
唯一索引可以确保集合的每一个文档的指定键都有唯一值。例如,如果想保证同不文档的"username"
键拥有不同的值,创建一个唯一索引就好了:
> db.users.ensureIndex({"username" : 1}, {"unique" : true})
如果试图向上面的集合中插入如下文档:
> db.users.insert({username: "bob"})
> db.users.insert({username: "bob"})
E11000 duplicate key error index: test.users.$username_1 dup key: { : "bob" }
如果检查这个集合,会发现只有第一个"bob"
被保存进来了。发现有重复的键时抛出异常会影响效率,所以可以使用唯一索引来应对偶尔可能会出现的键重复问题,而不是在运行时对重复的键进行过滤。
有一个唯一索引可能你已经比较熟悉了,就是"_id"
索引,这个索引会在创建集合时自动创建。这就是一个正常的唯一索引(但它不能被删除,而其他唯一索引是可以删除的)。
如果一个文档没有对应的键,索引会将其作为
null
存储。所以,如果对某个键建立了唯一索引,但插入了多个缺少该索引键的文档,由于集合已经存在一个该索引键的值为null
的文档而导致插入失败。5.4.2节会详细介绍相关内容。
有些情况下,一个值可能无法被索引。索引储桶(index bucket)的大小是有限制的,如果某个索引条目超出了它的限制,那么这个条目就不会包含在索引里。这样会造成一些困惑,因为使用这个索引进行查询时会有一个文档凭空消失不见了。所有的字段都必须小于1024字节,才能包含到索引里。如果一个文档的字段由于太大不能包含在索引里,MongoDB不会返回任何错误或者警告。也就是说,超出8 KB大小的键不会受到唯一索引的约束:可以插入多个同样的8 KB长的字符串。
1. 复合唯一索引
也可以创建复合的唯一索引。创建复合唯一索引时,单个键的值可以相同,但所有键的组合值必须是唯一的。
例如,如果有一个{"username" : 1, "age" : 1}
上的唯一索引,下面的插入是合法的:
> db.users.insert({"username" : "bob"})
> db.users.insert({"username" : "bob", "age" : 23})
> db.users.insert({"username" : "fred", "age" : 23})
然而,如果试图再次插入这三个文档中的任意一个,都会导致键重复异常。
GirdFS是MongoDB中存储大文件的标准方式(详见6.5节),其中就用到了复合唯一索引。存储文件内容的集合有一个{"files_id" : 1, "n" : 1}
上的复合唯一索引,因此文档的某一部分看起来可能会是下面这个样子:
{"files_id" : ObjectId("4b23c3ca7525f35f94b60a2d"), "n" : 1}
{"files_id" : ObjectId("4b23c3ca7525f35f94b60a2d"), "n" : 2}
{"files_id" : ObjectId("4b23c3ca7525f35f94b60a2d"), "n" : 3}
{"files_id" : ObjectId("4b23c3ca7525f35f94b60a2d"), "n" : 4}
注意,所有"files_id"
的值都相同,但是"n"
的值不同。
2. 去除重复
在已有的集合上创建唯一索引时可能会失败,因为集合中可能已经存在重复值了:
> db.users.ensureIndex({"age" : 1}, {"unique" : true})
E11000 duplicate key error index: test.users.$age_1 dup key: { : 12 }
通常需要先对已有的数据进行处理(可以使用聚合框架),找出重复的数据,想办法处理。
在极少数情况下,可能希望直接删除重复的值。创建索引时使用"dropDups"
选项,如果遇到重复的值,第一个会被保留,之后的重复文档都会被删除。
> db.people.ensureIndex({"username" : 1}, {"unique" : true, "dropDups" : true})
"dropDups"
会强制性建立唯一索引,但是这个方式太粗暴了:你无法控制哪些文档被保留哪些文档被删除(如果有文档被删除的话,MongoDB也不会给出提示说哪些文档被删除了)。对于比较重要的数据,千万不要使用"dropDups"
。
5.4.2 稀疏索引
前面的小节已经讲过,唯一索引会把null
看做值,所以无法将多个缺少唯一索引中的键的文档插入到集合中。然而,在有些情况下,你可能希望唯一索引只对包含相应键的文档生效。如果有一个可能存在也可能不存在的字段,但是当它存在时,它必须是唯一的,这时就可以将unique
和sparse
选项组合在一起使用。
MongoDB中的稀疏索引(sparse index)与关系型数据库中的稀疏索引是完全不同的概念。基本上来说,MongoDB中的稀疏索引只是不需要将每个文档都作为索引条目。
使用sparse
选项就可以创建稀疏索引。例如,如果有一个可选的email地址字段,但是,如果提供了这个字段,那么它的值必须是唯一的:
> db.ensureIndex({"email" : 1}, {"unique" : true, "sparse" : true})
稀疏索引不必是唯一的。只要去掉unique
选项,就可以创建一个非唯一的稀疏索引。
根据是否使用稀疏索引,同一个查询的返回结果可能会不同。假如有这样一个集合,其中的大部分文档都有一个"x"
字段,但是有些没有:
> db.foo.find()
{ "_id" : 0 }
{ "_id" : 1, "x" : 1 }
{ "_id" : 2, "x" : 2 }
{ "_id" : 3, "x" : 3 }
当在"x"
上执行查询时,它会返回相匹配的文档:
> db.foo.find({"x" : {"$ne" : 2}})
{ "_id" : 0 }
{ "_id" : 1, "x" : 1 }
{ "_id" : 3, "x" : 3 }
如果在"x"
上创建一个稀疏索引,"_id"
为0的文档就不会包含在索引中。如果再次在"x"
上查询,MongoDB就会使用这个稀疏索引,{"_id" : 0}
的这个文档就不会被返回了:
> db.foo.find({"x" : {"$ne" : 2}})
{ "_id" : 1, "x" : 1 }
{ "_id" : 3, "x" : 3 }
如果需要得到那些不包含"x"
字段的文档,可以使用hint()
强制进行全表扫描。
5.5 索引管理
如前面的小节所述,可以使用ensuerIndex
函数创建新的索引。对于一个集合,每个索引只需要创建一次。如果重复创建相同的索引,是没有任何作用的。
所有的数据库索引信息都存储在system.indexes
集合中。这是一个保留集合,不能在其中插入或者删除文档。只能通过ensureIndex
或者dropIndexes
对其进行操作。
创建一个索引之后,就可以在system.indexes
中看到它的元信息。可以执行db.collectionName.getIndexes()
来查看给定集合上的所有索引信息:
> db.foo.getIndexes()
[
{
"v" : 1,
"key" : {
"_id" : 1
},
"ns" : "test.foo",
"name" : "_id_"
},
{
"v" : 1,
"key" : {
"y" : 1
},
"ns" : "test.foo",
"name" : "y_1"
},
{
"v" : 1,
"key" : {
"x" : 1,
"y" : 1
},
"ns" : "test.foo",
"name" : "x_1_y_1"
}
]
这里面最重要的字段是"key"
和"name"
。这里的键可以用在hint
、max
、min
以及其他所有需要指定索引的地方。在这里,索引的顺序很重要:{"x" : 1, "y" : 1}
上的索引与{"y" : 1, "x" : 1}
上的索引不同。对于很多的索引操作(比如dropIndex
),这里的索引名称都可以被当作标识符使用。但是这里不会指明索引是否是多键索引。
"v"
字段只在内部使用,用于标识索引版本。如果你的索引不包含"v" : 1
这样的字段,说明你的索引是以一种效率比较低的旧方式存储的。将MongoDB升级到至少2.0版本,删除并重建这些索引,就可以把索引的存储方式升级到新的格式了。
5.5.1 标识索引
集合中的每一个索引都有一个名称,用于唯一标识这个索引,也可以用于服务器端来删除或者操作索引。索引名称的默认形式是key name1_dir1_keyname2_dir2_…_keynameN_dirN
,其中keynameX
是索引的键,dirX
是索引的方向(1或者-1)。如果索引中包含两个以上的键,这种命名方式就显得比较笨重了,好在可以在ensureIndex
中指定索引的名称:
> db.foo.ensureIndex({"a" : 1, "b" : 1, "c" : 1, ..., "z" : 1},
... {"name" : "alphabet"})
索引名称的长度是有限制的,所以新建复杂索引时可能需要自定义索引名称。调用getLastError
就可以知道索引是否成功创建,或者失败的原因。
5.5.2 修改索引
随着应用不断增长变化,你会发现数据或者查询已经发生了改变,原来的索引也不那么好用了。这时可以使用dropIndex
命令删除不再需要的索引:
> db.people.dropIndex("x_1_y_1")
{ "nIndexesWas" : 3, "ok" : 1 }
用索引描述信息里"name"
字段的值来指定需要删除的索引。
新建索引是一件既费时又浪费资源的事情。默认情况下,MongoDB会尽可能快地创建索引,阻塞所有对数据库的读请求和写请求,一直到索引创建完成。如果希望数据库在创建索引的同时仍然能够处理读写请求,可以在创建索引时指定background
选项。这样在创建索引时,如果有新的数据库请求需要处理,创建索引的过程就会暂停一下,但是仍然会对应用程序性能有比较大的影响(12.4.8节会详细介绍)。后台创建索引比前台创建索引慢得多。
在已有的文档上创建索引会比新创建索引再插入文档快一点。
第18章会更详细地介绍实际创建索引。