MongoDB系列四(索引).
一、索引简介
再来老生常谈一番,什么是索引呢?数据库索引与书籍的索引类似。有了索引就不需要翻整本书,数据库可以直接在索引中查找,在索引中找到条目以后,就可以直接跳转到目标文档的位置,这能使查找速度提高几个数量级。
然而,使用索引是有代价的:对于添加的每一个索引,每次写操作(插入、更新、删除)都将耗费更多的时间。这是因为,当数据发生变动时,MongoDB不仅要更新文档,还要更新集合上的所有索引。因此,MongoDB限制每个集合上最多只能有64个索引。通常,在一个特定的集合上,不应该拥有两个以上的索引。于是,挑选合适的字段建立索引非常重要。
- 索引基数
基数(cardinality)就是集合中某个字段拥有不同值的数量。比如 gender 字段,基数一般就男女 2个而已;而像 mobile 这样的字段,基数就会特别大。
通常来讲,一个字段的基数越高,这个字段上的索引就越有用。这是因为索引能够迅速将搜索范围缩小到一个比较小的结果集。对于低基数的字段,索引通常无法排除掉大量可能的匹配。假设我们在"gender"上有一个索引,需要查找名为Susan的女性用户。通过这个索引,只能将搜索空间缩小到大约50%。
tips:在关系型数据库中类似 gender 这样的字段可以使用位图索引。
- 索引原理浅析
我们以一个索引 {"age" : 1, "username" : 1} 来看看索引在MongoDB 中是如何存储的,大致是这个样子:
每一个索引条目都包含一个"age"字段 和 "username"字段,并且指向文档在磁盘中的存储位置。注意,这里的 age 严格的按照升序排序,并且相同的 age 对应的 username 也严格的按照升序排序。
来看个例子 :db.users.find({"age" : 21}).sort({"username" : -1})
这个索引对于这个查询来说是非常高效的,因为它可以马上定位到 age = 21 的位置,并且age = 21 中的 username 已经是排序好的。
tips:排序方向并不重要:MongoDB可以在任意方向上对索引进行遍历。
tips:查询中的字段顺序无关紧要,MongoDB 会自动找出可以使用索引的字段,而无视查询的字段顺序。
- $操作符如何使用索引
有一些查询完全无法使用索引,也有一些查询能够比其他查询更高效地使用索引。
$where:无法使用索引。
$nin:无法使用索引。
$exists:无法使用索引。因为在索引中,不存在的字段和null字段的存储方式是一样的,查询必须遍历每一个文档检查这个值是否真的为null还是根本不存在。
$ne:可以使用索引,但并不是很高效。因为必须遍历整个索引条目才能找到结果的文档。
$not:能够使用索引,但通常不知道如何使用索引,从而退化成全表扫描。
$or:能够使用索引,但是$or 查询会将 or 的条件拆分成多个独立的查询,然后再将结果合并在一起。这是很低效的,不建议用。建议用 $in 取代 $or 。
设计多键索引的时候要记得,要把基数大的字段放在索引的前面,因为这样能更快缩小查询的范围。
二、索引类型
- 复合(组合)索引
复合索引就是一个建立在多个字段上的索引。
如果查询中有多个排序方向或者查询条件中有多个键,复合索引就非常有效。
db.userInfo.ensureIndex({"age":1,"age":1})
进行多键排序时,索引的方向尤为重要。尽量做到多键排序的方向和复合索引的方向是一致的,因为这能很大的避免在内存中进行排序的运算。
tips:相互反转(在每个方向上都乘以-1)的索引是等价的:{"age" : 1, "user name" : -1}适用的查询与{"age" : -1, "username" : 1}是完全一样的。
复合索引具有双重功能,而且对不同的查询可以表现为不同的索引。如果有一个{"age" :1, "username" : 1}索引,"age"字段会被自动排序,就好像有一个{"age" : 1}索引一样。因此,这个复合索引可以当作{"age" : 1}索引一样使用。
- 唯一索引
唯一索引可以确保集合的每一个文档的指定键都有唯一值。我们熟悉的 "_id" 索引就是一个唯一索引(但它不能被删除,而其他唯一索引是可以删除的)。
db.users.ensureIndex({"username" : 1}, {"unique" : true})
定义了唯一索引后,这个键就不允许插入重复的值了,否则会抛异常。
tips:A 字段不存在 和 A 字段为 null 是互斥的!
在已有的集合上创建唯一索引可能会报错,因为集合中可能已经有重复的值了。在极少数情况下,可能希望直接删除重复的值。创建索引时使用"dropDups"选项,如果遇到重复的值,第一个会被保留,之后的重复文档都会被删除。
db.users.ensureIndex({"username" : 1}, {"unique" : true, "dropDups" : true})
- 稀疏索引
在有些情况下,你可能希望唯一索引只对包含相应键的文档生效。如果有一个可能存在也可能不存在的字段,但是当它存在时,它必须是唯一的,这时就可以将unique和sparse选项组合在一起使用,创建唯一稀疏索引。注意:MongoDB中的稀疏索引(sparse index)与关系型数据库中的稀疏索引是完全不同的概念。基本上来说,MongoDB中的稀疏索引只是不需要将每个文档都作为索引条目。并且,稀疏索引并不一定是唯一的。
db.ensureIndex({"email" : 1}, {"unique" : true, "sparse" : true})
当某个查询使用了稀疏索引,就不会返回不包含这个字段的文档。因为稀疏索引并没有把每个文档都作为索引条目。
- 覆盖索引
如果你的查询只需要查找索引中包含的字段,那就根本没必要获取实际的文档。当一个索引包含用户请求的所有字段,可以认为这个索引覆盖了本次查询。所以,尽可能使用投射筛选返回的字段,比如 {"_id":0,"age":1} 等,来实现覆盖索引。
三、索引管理
- 新建索引
普通索引
db.userInfo.ensureIndex({"name":1},{"name","MyIndex"})
"1" 表示按照name进行升序,"-1" 表示按照name进行降序。
默认的索引以 key1_1_key2_-1 这样的方式命名,可以手动指定索引的名字,如上。
对象索引
可以对整个对象建立索引,或者对对象的某个元素使用索引。
db.users.ensureIndex({"loc" : 1})
只有在进行与对象字段顺序完全匹配的子文档查询时(比如db.users.find({"loc" :{"ip" : "123.456.789.000", "city" : "Shelbyville", "state" :"NY"}}})),查询优化器才会使用"loc"上的索引。
db.users.ensureIndex({"loc.city" : 1})
有涉及到对象city的查询都会使用这个索引。
数组索引
对数组建立索引,实际上是对数组的每个元素建立一个索引条目。比如一个文档中的数组字段有20个元素,那么该文档就拥有了20个索引条目!所以对数组字段的索引建立要慎重。
- 删除索引
db.userInfo.dropIndexes("name_1")
删除指定索引
db.userInfo.dropIndexes()
删除除了_id 以外的所有索引
- 操作索引
获取当前索引列表:db.userInfo.getIndexes()
hint 暴力选择某种索引:db.userInfo.find({name:'zhangsan',birthday:'1989-3-2'}).hint({"name":1,"birthday":1})
强制使用全表扫描:db.userInfo.find({"birthday" : {"$lt" :"1989-3-2"}}).hint({"$natural" : 1})
索引分析函数explain:MongoDB 3.0前 和 MongoDB 3.0后存在很大的差异,这里只简单说明下,如果想详细了解的话,可以关注该作者的文章:
MongoDB 3.0 前:db.driverLocation.find({"areaCode":"350203"}).explain()
cursor:表扫描方式 (basicCursor:顺序查找)
nscanned:浏览了多少文档
n:最终返回了几个文档
millis:总共耗时了多少毫秒
scanAndOrder:是否必须在内存中对数据进行排序
MongoDB 3.0 后:db.driverLocation.find({"areaCode":"350203"}).explain("executionStats")
executionTimeMillis:该query的整体查询时间
nReturned:查询返回的条目
totalKeysExamined:索引扫描条目
totalDocsExamined:文档扫描条目