MongoDB 索引
索引是用来加快查询的,数据库索引与数据的索引类似,有了索引就不需要翻遍整本书,数据库可以直接在索引中查找,
使得查询速度很快,在索引中找到条目后,就可以直接跳转到目标文档的位置.
1.索引简介
要掌握如何为查询配置最佳索引会有些难度.
MongoDB索引几乎和关系型数据库的索引一样.绝大数优化关系型数据库索引的技巧同样适用于MongoDB.
如:
db.refactor.insert({"username":"refactor","age":24,"isactive":true})
db.refactor.insert({"username":"refactor","age":30,"isactive":false})
db.refactor.insert({"username":"aaaaa","age":24,"isactive":false})
db.refactor.insert({"username":"aaaaa","age":34,"isactive":true})
db.refactor.insert({"username":"sssssss","age":24,"isactive":true})
db.refactor.insert({"username":"tttttt","age":24,"isactive":true})
db.refactor.insert({"username":"tttttt","age":54,"isactive":true})
db.refactor.insert({"username":"bbbbb","age":24,"isactive":false})
db.refactor.insert({"username":"rrrrr","age":24,"isactive":true})
db.refactor.insert({"username":"rrrrr","age":54,"isactive":false})
要按照username键进行查找,就可以在此键上建立索引,来提高查询速度.
db.refactor.ensureIndex({"username":1})
对某个键创建索引会加速对该键的查询,但是对于其他的查询可能没有帮助,即便查询中包含了被索引的键.
db.refactor.find({"age":24}).sort({"age":1,"username":1})
不会用到username索引.服务器必须查找所有文档,找到想要的日期,这个过程叫:表扫描,就是在没有索引的书中查找
内容,要从第一页开始,从前翻到后.通常说,应避免让服务器做表扫描,因为集合很大时会很慢.
一定要创建查询中用到的所有键索引,对于上面的查询,应该建立age和username的索引.
db.refactor.ensureIndex({"age":1,"username":1})
传递给ensureIndex的文档是一组值为1或-1的键,表示索引的创建方向.若索引只有一个键,则方向无关紧要.
若是有多个键,就得考虑索引的方向问题了.
如:
> db.runCommand({"dropIndexes":"refactor","index":"*"})
{
"nIndexesWas" : 2,
"msg" : "non-_id indexes dropped for collection",
"ok" : 1
}
> db.refactor.ensureIndex({"username":1,"age":1})
> db.refactor.ensureIndex({"username":1,"age":-1})
> db.system.indexes.find()
{ "v" : 1, "key" : { "_id" : 1 }, "ns" : "test.blog", "name" : "_id_" }
{ "v" : 1, "key" : { "_id" : 1 }, "ns" : "test.refactor", "name" : "_id_" }
{ "v" : 1, "key" : { "_id" : 1 }, "ns" : "test.users", "name" : "_id_" }
{ "v" : 1, "key" : { "username" : 1, "age" : 1 }, "ns" : "test.refactor", "name"
: "username_1_age_1" }
{ "v" : 1, "key" : { "username" : 1, "age" : -1 }, "ns" : "test.refactor", "name
" : "username_1_age_-1" }
如果以{"username":1,"age":1}这种方式创建索引,MongoDB会按如下方式组织:
> db.refactor.find().hint({"username":1,"age":1})
{ "_id" : ObjectId("500231f4218b8ef3edbc6f00"), "username" : "aaaaa", "age" : 24
, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f01"), "username" : "aaaaa", "age" : 34
, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f05"), "username" : "bbbbb", "age" : 24
, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6efe"), "username" : "refactor", "age" :
24, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6eff"), "username" : "refactor", "age" :
30, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f06"), "username" : "rrrrr", "age" : 24
, "isactive" : true }
{ "_id" : ObjectId("500231f6218b8ef3edbc6f07"), "username" : "rrrrr", "age" : 54
, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f02"), "username" : "sssssss", "age" :
24, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f03"), "username" : "tttttt", "age" : 2
4, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f04"), "username" : "tttttt", "age" : 5
4, "isactive" : true }
用户名按照字母升序排列,同名的组按照年龄升序排列.
如果以{"username":1,"age":-1}这种方式创建索引,MongoDB会按如下方式组织:
> db.refactor.find().hint({"username":1,"age":-1})
{ "_id" : ObjectId("500231f4218b8ef3edbc6f01"), "username" : "aaaaa", "age" : 34
, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f00"), "username" : "aaaaa", "age" : 24
, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f05"), "username" : "bbbbb", "age" : 24
, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6eff"), "username" : "refactor", "age" :
30, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6efe"), "username" : "refactor", "age" :
24, "isactive" : true }
{ "_id" : ObjectId("500231f6218b8ef3edbc6f07"), "username" : "rrrrr", "age" : 54
, "isactive" : false }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f06"), "username" : "rrrrr", "age" : 24
, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f02"), "username" : "sssssss", "age" :
24, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f04"), "username" : "tttttt", "age" : 5
4, "isactive" : true }
{ "_id" : ObjectId("500231f4218b8ef3edbc6f03"), "username" : "tttttt", "age" : 2
4, "isactive" : true }
用户名按照字母升序排列,同名的组按照年龄降序排列.
一般来说,如果索引包含了N个键,则对于前几个键的查询都能利用索引,如:有个索引{"a":1,"b":1,"c":1,"d":1}
实际上是有了{"a":1},{"a":1,"b":1},{"a":1,"b":1,"c":1}索引,但是使用{"b":1},{"a":1,"c":1}等索引的查询不会被优化.
只有使用索引前部查询才能使用该索引.
MongoDB的查询优化器会从排查询项的顺序,以便利用索引,如查询{"username":"refactor","age":24}的时候,已经有了
{"age":1,"username":1}的索引,MongoDB会自己找到并利用它.
创建索引的缺点是每次插入,更新,删除都会产生额外的开销,因为数据库不但需要执行这些操作,还要将这些操作在集合的索引中
标记.因此,尽可能少的创建索引.
有些时候,最有效的查询是不实用查询,一般来说,要是查询要返回集合中一半以上的结果,用表扫描会比几乎每条文档都要
索引要快,所以,查询是否存在某个键,或者检查摸个布尔类型的值是真是假,就没有必要利用索引.
2.扩展索引
假设有个集合存储了用户的状态信息.现在要查询用户和日期,取出某一用户最近的状态.我们可能会建立
如下索引:
db.users.ensureIndex({"user":1,"date":-1})
这会使对用户和日期的查询非常快,但是并不是最好的方式.
因为应用会有数百万的用户,每人每天都有数十条状态更新.若是每条用户状态的索引值咱用类似一页纸的
磁盘控件,那么对每次"最新状态"的查询,数据库将会将不同的页载入内存.若是站点太热门,内存放不下所有
索引,就会很慢.要是改变索引的顺序{"date":-1,"user":1},则数据库可以将最后几天的索引保存在内存中,
可以有效的减少内存交换,这样查询任何用户的最新状态都会快很多.
3.索引内嵌文档中的键
为内嵌文档的键创建索引和为普通的键创建索引没有什么区别.
db.blog.insert(
{
"title":"refactor's blog",
"Content":"refactor's blog test",
"author":
{
"name":"refactor",
"email":"[email protected]"
}
}
)
为author.name创建索引
db.blog.ensureIndex({"author.name":1})
对内嵌文档的键索引和普通键索引没有区别,两者可以联合组成复合索引.
3.为排序创建索引
随着集合的增长,需要针对查询中大量的排序做索引.如果对没有索引的键调用sort,MongoDB需要将所有数据
提取到内存中来排序.因此,可以做无索引排序是有个上限的,即不可能在内存中对T级别的数据排序.按照排序来索引
以便MongoDB按照顺序提取数据,这样就能排序大规模数据,而不必担心用光内存.
4.索引名称
集合中的每个索引都有一个字符串类型的名字,来唯一标识索引,服务器通过这个名字来删除或操作索引.默认情况下,
索引名类似 keyname1_dir1_keyname2_dir2这种形式,其中keyname代表索引的键,dir代表索引的方向(1或-1).
可以通过ensureIndex来指定索引的名称.
如:
db.blog.ensureIndex({"author.name":1},{"name":"author_name_index"})
注意不能修改,只能删除索引,再重建.
索引名有字符个数的限制,所以特别复杂的索引在创建时一定要使用自定义的名字,可以用getLastError来检查索引
是否成功创建了或未创建成功的原因.
5.唯一索引
唯一索引可以确保集合的每一个文档的指定键都有唯一值.如果想保证文档的username键都有不同的值:
db.refactor.ensureIndex({"username":1},{"unique":true})
默认情况下,insert并不检查文档是否插入过了.所以为了避免插入的文档包含与唯一键重复的值,可能要用安全插入
才能满足要求,这样,在插入这样的文档会看到存在重复键错误的提示.
注意,如果文档中没有对应的键,索引会认为它是以null存储的,所以,如果对某个键建立了唯一索引,但插入了多个
缺少该索引键的文档,这由于文档包含null值而导致插入失败.
6.消除重复
当为已有的集合创建唯一索引,可能有些值已经重复了.这样唯一索引将创建失败.但是,可能希望将所有包含重复值
的文档都删掉.dropDups选项就可以保留发现的第一个文档,而删除接下来的有重复值的文档
db.refactor.ensureIndex({"username":1},{"unique":true,"dropDups":true})
如果有重要数据的话,最好还是写个脚本预处理,而不是设置dropDups
7.复合唯一索引
创建复合唯一索引,单个键的值可以重复,只要所有键的值组合起来不同就行.
GridFS是MongoDB中存储大文件的标准方式,其中就用到了复合唯一索引.
8.使用explain和hint
explain是一个非常有用的工具,会帮助你获得查询方面诸多信息.只要对游标调用该方法,可以得到查询细节.
explain会返回一个文档,而不是游标本身,这是与多数游标方法不同之处.
"cursor":"BtreeCursor age_1_username_1"
说明查询使用了age_1_username_1索引.
"nscanned" : 6
6 代表数据库查找了多少个文档.
"n" : 6
这个代表返回文档的数量
"millis" : 0
这个毫秒数表示数据库执行查询的时间.
可以通过索引名字age_1_username_1,来获取索引的详细信息.
db.system.indexes.find({"ns":"test.refactor","name":"age_1_username_1"})
如果 refactor集合有如下两个集合:
db.refactor.ensureIndex({"username":1,"age":1})
db.refactor.ensureIndex({"age":1,"username":1})
要查询用户的用户名和年龄:
db.refactor.find({"age":{"$gt":30},"username":"refactor"}).explain()
这个会用"username":1,"age":1的索引,因为是要求精确查询用户名和年龄范围,数据库自己调换了查询项的顺序.
db.refactor.find({"age":24,"username":/.*/}).explain()
这个会用"age":1,"username":1的索引
如果发现MongoDB用了非预期的索引,可以用hint强制使用某个索引.如:
db.refactor.find({"age":{"$gt":30},"username":"refactor"}).hint({"age":1,"username":1}).explain()
多说情况下,这种指定没有必要,MongoDB的查询优化器很智能,会替你选择用哪个索引.初次做某个查询时,
查询优化器会同时尝试各种查询方案.最先完成的被确定使用,其他的则终止掉.查询方案被记录下来,以备日后
应对相同键的查询.查询优化器定期重试其他方案,以防止因为添加新数据后,之前的方案不是最优了.只要关心
给查询优化器建立可以选择的索引就可以了.
9.索引管理
索引的元信息存储在每个数据库的system.indexes集合中.这是一个 保留集合(遍历数据库中所有集合时要小心,因为
通常我们不想对这个集合进行操作),不能对其插入或删除文档.操作只能通过ensureIndex或dropIndexes进行.
system.indexes集合中包含每个索引的详细信息.system.namespaces集合包含索引的名字.
10.修改索引
随着应用程序的使用,数据库的数据或查询发生了改变,原来的索引不在使用.可以使用ensureIndex随时向数据库
添加新的索引.
db.refactor.ensureIndex({"username":1,"age":1},"background":true)
建立索引即耗时又费力,还要消耗更多资源.使用{"background":true}选项可以使这个过程在后台完成
,同时正常处理请求.要是不使用background这个选项,数据库会阻塞建立索引期间的所有请求.
阻塞的做法会使索引建立的更快.即使在后台创建索引也会对正常操作有影响,所以最好选择无关紧要的时间.
为已由文档创建索引比先创建索引再插入所有文档要稍快一些.当然,要是集合的数据从无到有,事先创建一个索引.
要是索引没用了,可以使用dropIndexes加上索引名称将其删除.通常,要查一下system.indexes集合来找出索引名,
以为自动生成的名字会因驱动程序的不同而不同.
db.runCommand({"dropIndexes":"blog","index":"author.name_1"})
要删除所有索引
db.runCommand({"dropIndexes":"blog","index":"*"})
11.地理空间索引
随着移动设备的出现,找到离当前位置最近的N个场所的查询越来越多.MongoDB为坐标平面查询提供了专门
的索引,称作 地理空间索引
地理空间索引也是使用ensureIndex来创建,只不过不是"1"或"-1",而是"2d"
db.map.insert({"gps":[1,100]})
db.map.insert({"gps":{"x":-30,"y":30}})
db.map.insert({"gps":{"latitude":-60,"longitude":30}})
db.map.ensureIndex({"gps":"2d"})
"gps"键的值必须是某种形式的一对值:一个包含两个元素的数组或者是包含两个键的内嵌文档.内嵌文档
的键名可以是随意的,如{"gps":{"refactor":-60,"refactor1":30}
默认情况下,地理空间索引的值是-180~180(对经纬度很方便).要是想用其他值
db.map.ensureIndex({"gps":"2d"},{"min":-1000,"max":1000})
这样就创建了一个2000光年见方的空间索引.
地理空间查询有两种方式:
db.map.find({"gps":{"$near":[49,-49]}})
这会按照点(49,-49)由近及远的方式将map集合的所有文档返回.在没有指定limit值时,默认是100个文档.
要是不需要那么多结果,就应该设置一个少点的值以节约资源.
db.map.find({"gps":{"$near":[49,-49]}}).limit(1)
也可以使用:
db.runCommand({geoNear:"map",near:[49,-49],num:1})
geoNear还会返回每个文档到查询点的距离.这个距离是以你插入的数据为单位的,如果按照经纬度的角度插入,则
距离就是经纬度.find和"$near"组合不会给出距离,但若是结果大于4M,这是唯一的选择.
MongoDB不但能找到靠近一个点的文档,还能找到指定形状内的文档.做法是将原来的"$near"换成"$within".
"$within"获取形状作为参数.这些形状可以是 矩形,圆形等.
对于矩形:
db.map.find({"gps":{"$within":{"$box":[[10,20],[15,30]]}}})
"$box"的参数是两个元素的数组,一个元素指定了左下角的坐标,第二个指定右上角的坐标.
对于圆形:
db.map.find({"gps":{"$within":{"$center":[[12,25],5]}}})
12.复合地理空间索引
应用程序要找的东西经常不只是一个地点.可以将地理空间索引与普通索引组合起来.
MongoDB的地理空间索引假设索引的内容是在一个平面上的,也就是说,对于球体的地球,并不是很精确.