5.1 索引简介
数据库索引与书籍的索引类似。有了索引就不需要翻整本书,数据库可以直接在索引中查找,在索引中找到条目以后,就可以直接跳到目标文档的位置,这能使查询速度提高几个数量级。不使用索引的查询称为全表扫描。
> for(i=0;i<200;i++){
... db.users.insert(
... {
... "i":i,
... "username":"user"+i,
... "age":Math.floor(Math.random()*120),
... "created":new Date()
... }
... );
... }
WriteResult({ "nInserted" : 1 })
> db.users.find({username:"user101"}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.users",
"indexFilterSet" : false,
"parsedQuery" : {
"username" : {
"$eq" : "user101"
}
},
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"username" : {
"$eq" : "user101"
}
},
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "localhost.localdomain",
"port" : 27017,
"version" : "3.4.0",
"gitVersion" : "f4240c60f005be757399042dc12f6addbc3170c1"
},
"ok" : 1
}
> db.users.find({username:"user101"}).limit(1).explain()
索引可以根据给定的字段组织数据,让MongoDB能够非常快的找到目标文档。在username字段上创建一个索引:
>db.users.ensureIndex({"username":1})
> db.users.ensureIndex({"username":1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
> db.users.find({"username":"user101"}).explain()
5.1.1 复合索引简介
索引的值是按一定顺序排列的,只有在首先使用索引键进行排序时,索引才有用。下面的排序里"username"上的索引没什么作用:
> db.users.find().sort({"age":1,"username":1})
为了优化这个排序,可能需要在"age"和"username"上建立索引。
> db.users.ensureIndex({"age":1,"username":1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"ok" : 1
}
如果使用{"age":1,"username":1}建立索引,这个索引大致会是这个样子:
"age"相同的条目按照"username"升序排列
- db.users.find({"age":21}).sort({"username":-1})
这是一个点查询(point query),用于查找单个值。由于索引中的第二个字段,查询结果已经是有序的了:MongoDB可以从{"age":21}匹配的最后一个索引开始,逆序依次遍历索引:
- db.users.find({"age":{"$gte":21,"$lte":30}})
这是一个多值查询(multi-value query):
- db.users.find({"age":{"$gte":21,"$lte":30}}).sort({"username":1})
使用这个索引得到的结果集中"usernaem"是无序的。
> db.users.find({"age":{"$gte":21,"$lte":30}}).
... sort({"username":1}).
... explain() // 通过explain()查看MongoDB对db.users.find(...)的默认行为
可以通过hint来强制MongoDB使用某个特定的索引,再次执行这个查询,但是这次使用{"username":1,"age":1}作为索引。这个查询扫描的文档比较多,但是不需要在内存中对数据排序:
> db.users.find({"age":{"$gte":21,"$lte":30}}).
... sort({"username":1}).
... hint({"username":1,"age":1}).
... explain()
5.1.2 使用复合索引
1.选择键的方向
{"age":1,"username":-1}
2.使用覆盖索引(covered index)
当一个索引包含用户请求的所有字段,可以认为这个索引覆盖了本次查询。如果在覆盖索引上执行explain(),"indexOnly"字段的值要为true。
3.隐式索引
复合索引具有双重功能,而且对不同的查询可以表现为不同的索引。如果有一个{"age":1,"username":1}索引,"age"字段会被自动排序,就好像有一个{"age":1}索引一样。
5.1.3 $操作符如何使用索引
1.低效率的操作符
有一些查询完全无法使用索引,比如"$where"查询和检查一个键是否存在的查询({"key":{"$exists":true}})。
通常来说,取反的效率是比较低的。大多数使用"$not"的查询都会退化为进行全表扫描。"$nin"就总是进行全表扫描。
2.范围
这个查询会直接定位到"age"为47的索引条目,然后在其中搜索用户名介于"user5"和"user8"的条目。
反过来,假如使用{"username":1,"age":1}索引,这样就改变了查询计划,查询必须先找到介于"user5"和"user8"之间的所有用户,然后再从中挑选"age"等于47的用户。
3.OR查询
>db.foo.find({"$or":[{"x":123},{"y":456}]).explain()
"$or"实际上是执行两次查询然后将结果集合并。可以看到,这次的explain()输出结果由两次独立的查询组成。通常来说,执行两次查询再将结果合并的效率不如单词查询高,因此,应该尽可能使用"$in"而不是"$or"。5.1.4 索引对象和数组
1.索引嵌套文档
需要在"loc"的某一个子字段(比如"loc.city")上建立索引,以便提高这个字段的查询速度:
>db.users.ensureIndex({"loc.city":1})
对嵌套文档本身("loc")建立索引,与对嵌套文档的某个字段("loc.city")建立索引是不同的。对整个子文档建立索引,只会提高整个子文档的查询速度。
2.索引数组
在博客文章集合中嵌套的"comments"数组的"date"键上建立索引:
>db.blog.ensureIndex({"comments.date":1})
对数组建立索引,实际上是对数组中的每个元素建立索引,而不是对数组本身建立索引。
一个索引中的数组字段最多只能有一个。假如有一个{"x":1,"y":1}上的索引:
如果MongoDB要为上面的最后一个例子创建索引,它必须要创建这么多索引条目:
3.多键索引
对于某个索引的键,如果这个键在某个文档是一个数组,那么这个索引就会被标记为多键索引(multikey index)。可以从explain()的输出中看到一个索引是否为多键索引:如果使用了多键索引,"isMultikey"字段的值会是true。
5.1.5 索引基数
基数(cardinality)就是集合中某个字段拥有不同值的数量。通常,一个字段的基数越高,这个键上的索引就越有用。这是因为索引能够迅速将搜索范围缩小到一个比较小的结果集。对于低基数的字段,索引通常无法排除掉大量可能的匹配。
5.2 使用explain()和hint()
expalin()能够提供大量与查询相关的信息。对于速度比较慢的查询来说,这是最重要的诊断工具之一。最常见的explain()输出有两种类型:使用索引的查询和没有使用索引的查询。
对于使用了复合索引的查询,最简单情况下的explain()输出如下所示:
- "cursor":"BtreeCursor age_1_username_1":BtreeCursor表示本次查询使用了索引,具体来说,是使用了"age"和"username"上的索引{"age":1,"username":1}。
- "isMultiKey":false:用于说明本次查询是否使用了多键索引
- "n":8332:本次查询返回的文档数量
- "nscannedObjects":8332:如果有使用索引,那么这个数字就是查找过的索引条目数量。如果是全表扫描,那么这个数字就表示检查过的文档数量。
- "scanAndOrder":false:是否在内存中对结果集进行了排序
- "indexOnly":false:是否只使用索引就能完成此次查询
- "nYields":0:为了让写入请求能够顺利执行,本次查询暂停的次数。
- "millis":91:数据库执行本次查询所耗费的毫秒数。
- "indexBounds":{...}:索引的遍历范围
假如有一个{"username":1,"age":1}上的一个索引和一个{"age":1,"username":1}上的索引。同时查询"username"和"age"时:
可以使用hint()强制MongoDB使用特定的索引。例如,如果希望MongoDB在上个例子的查询中使用{"username":1,"age":1}索引,可以:
>db.c.find({"age":14,"username":/.*/}).hint({"username":1,"age":1})
5.3 何时不应该使用索引
5.4 索引类型
5.4.1 唯一索引
唯一索引可以确保集合的每一个文档的指定键都有唯一值。
> db.users.ensureIndex({"username":1},{"unique":true})
1.复合唯一索引
创建复合唯一索引时,单个键的值可以相同,但所有键的组合值必须是唯一的。
如果试图再次插入这三个文档中的任意一个,都会导致键重复异常。
2.去除重复
在已有的集合上创建唯一索引可能会失败:
创建索引时使用"dropDups"选项,如果遇到重复的值,第一个会被保留,之后的重复文档都会被删除。
5.4.2 稀疏索引
使用sparse选项就可以创建稀疏索引。例如,如果有一个可选的email地址字段,但是,如果如果提供了这个字段,那么它的值必须是唯一的:
>db.ensureIndex({"email":1},{"unique":true,"sparse":true})
稀疏索引不必是唯一的。只要去掉unique选项,就可以创建一个非唯一的稀疏索引。
如果在"x"上创建一个稀疏索引,"_id"为0的文档就不会包含在索引中。
5.5 索引管理
所有的数据库索引信息都存储在system.indexes集合中。只能通过ensureIndex或者dropIndexes对其进行操作。
可以运行db.collectionName.getIndexes()查看给定集合上的所有索引信息。
标识索引:
修改索引: