MongoDB学习笔记四:索引

时间:2021-07-16 03:32:05

索引就是用来加速查询的。创建数据库索引就像确定如何组织书的索引一样。但是你的优势是知道今后做何种查询,以及哪些内容需要快速查找。比如:所有的查询都包括"date"键,那么很可能(至少)需要建立一个关于"date"的索引。如果要查询用户名,则不必索引"user_num"键,因为根本不会对其进行查询。
现在要依照某个键进行查找:
> db.people.find({"username" : "mark"})
当查询中仅使用一个键时,可以对该键建立索引,以提高查询速度。这里对"username"建立索引。创建索引要使用ensureIndex方法:
> db.people.ensureIndex({"username" : 1})
对于同一个集合,同样的索引只需要创建一次。反复创建是徒劳的。
对某个键创建的索引会加速对该键的查询。然而,对于其他查询可能没有帮助,即便是查询包含了被索引的键。例如,下面的查询就不会从先前建立的索引中获得任何性能的提升:
> db.people.find({"date" : date1}).sort({"date" : 1, "username" : 1})
服务器必须"查找整本书"找到想要的日期。这个过程称作"表扫描",就是在没有索引的书中找内容,要从第一页开始,从前到后翻。通常来讲,要尽量避免让服务器做表扫描,因为当集合很大时会非常慢。
★一定要创建查询中用到的所有键的索引。例如,对于上面的查询,应该建立日期和用户名的索引:
> db.ensureIndex({"date" : 1, "username" : 1})
传递给ensureIndex的文档其形式与传递给sort的文档形式一样:一组值为1或者-1的键——在索引存在多个键的时候需要考虑索引的方向问题。
MongoDB的查询优化器会重排查询项的顺序,一边利用索引:比如查询{"x" : "foo", "y" : "bar"}的时候,已经有了{"y" : 1, "x" : 1}的索引,MongoDB会自己找到并利用它。
创建索引的缺点就是每次插入、更新和删除时都会产生额外的开销。这是因为数据库不但需要执行这些操作,还要将这些操作在集合的索引中标记。因此,要尽可能少创建索引。
每个集合默认的最大索引个数为64个。
『扩展索引』
假设我们有一个集合,保存了用户的状态信息。现在想要查询用户和日期,取出某一用户最近的状态。以我们目前所学,我们会像下面这样创建一个索引:
> db.status.ensureIndex({user : 1, date : -1})
这会使用户和日期的查询非常快,但是并不是最好的方式。
再想想书籍的索引。有一组文档按照用户名(升序)排序,而后按照日期(降序)排序,所以会是这种情形:
User 123 on March 13, 2010
User 123 on March 12, 2010
User 123 on March 11, 2010
User 123 on March 5, 2010
User 123 on March 4, 2010
User 124 on March 12, 2010
User 124 on March 11, 2010
...
这点数据看着还行,但是应用会有数百万的用户,每人每天有数十条状态更新。若是每条用户状态的索引值占用类似一页纸的磁盘空间,那么对于每次最新状态的查询,数据库都会讲不同的页载入内存。若是站点太热门,内存放不下所有的索引,就会非常非常慢。
要是改变顺序,编程{date : -1, user : 1},则数据库可以将最后几天的索引保存在内存中,可以有效减少内存交换,这样查询任何用户的最新状态都会快很多。
所以,建立索引时要考虑如下问题:
(1)会做什么样的查询?其中那些键需要索引?
(2)每个键的索引方向是怎么样的?
(3)如何应对扩展?有没有不同的键的排列可以是常用数据更多地保留在内存中?
索引内嵌文档中的键
为内嵌文档的键建立索引和为普通的键建立索引没有什么区别。例如,要想按日期搜索博客文章的评论,可以在由内嵌的"comments"文档组成的数组中对"date"键创建索引:
> db.blog.ensureIndex({"comments.date" : 1})
对内嵌文档的键索引与普通文档的键索引并无差异,两者也可以联合组成复合索引。
为排序创建索引
随着集合的增长,需要针对查询中大量的排序作索引。如果对没有索引的键调用sort,MongoDB需要将所有数据提取到内存来排序。因此,可以做无索引排序是有个上限的。
按照排序来索引以便让MongoDB按照顺序提取数据,这样就能排序大规模数据,而不必担心用光内存。
索引名称
集合中的每个索引都有一个字符串类型的名字,来唯一标识索引,服务器通过这个名字来删除或者操作索引。
默认情况下,索引名类似keyname1_dir1_keyname2_dir2_.._keynameN_dirN这种形式,其中keynameX代表索引的键,dirX代表索引的方向(1或者-1)。要是索引的得键特别多,这样命名就略显愚笨了,但是可以通过ensureIndex的选项来指定自定义的名字:
> db.foo.ensureIndex({"a" : 1, "b" : 1, "c" : 1, ..., "z" : 1}, {"name" : "alphabet"})
索引名有字符个数的限制,所以特别复杂的索引在创建的时候一定要使用自定义的名字。可以用getLastError来检查索引是否成功创建了或者未成功创建的原因。
『唯一索引』
唯一索引可以确保集合的每一个文档的指定键都有唯一值。例如,如果想保证文档的"username"键都有不一样的值,创建一个唯一索引就好了:
> db.people.ensureIndex({"username" : 1}, {"unique" : true})
注意:insert并不检查文档是否插入过了。所以,为了避免插入的文档中包含与唯一键重复的值,可能要用安全插入才能满足要求。这样,在插入这样的文档时会看到存在重复键错误的提示。
最熟悉的唯一索引:"_id"——这个索引是在创建普通集合时一同创建的,这个索引和普通唯一索引只有一点不同,就是不能删除。
消除重复
创建唯一索引时可能有些键重复。dropDups选项可以保留发现的第一个文档,而删除接下来的有重复值的文档:
> db.people.ensureIndex({"username" : 1}, {"unique" : true, "dropDups" : true})
复合唯一索引
创建复合唯一索引的时候,单个键的值可以相同,只要所有键的值组合起来不同就好。
例:GridFS是MongoDB中存储大文件的标准方式,其中就用到了复合唯一索引。存储文件内容的集合有一个复合唯一索引{filed_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"的值不同。若是试图再次插入{files_id : ObjectId("4b23c3ca7525f35f94b60a2d"), n : 1},则数据库会提示存在重复键的错误。
explain:对游标使用该方法,就可以得到查询细节。explain会返回一个文档,而不是游标本身,这是与多数游标方法不同之处。
> db.foo.find().explain()
explain会返回查询使用的索引情况(如果有的话),耗时及扫描文档数的统计信息。
例如,索引{"username" : 1}对单个键的查询非常有帮助,但是多数查询要复杂得多。比如,要做如下查询并排序:
> db.people.find({"age", 18}).sort({"username" : 1})
这是就搞不太清楚数据库到第用没用到已经创建的索引,或者效率如何。使用explain就会得到当前查询所使用的索引,消耗了多少时间,以及数据库需要扫描多少文档才能得到结果。
『explain的一个例子』
对于一个只有64个文档,没有索引("_id"索引除外)的数据库,做一次最简单的查询({}),explain的输出类似如下:
> db.people.find().explain()
{
"cursor" : "BasicCursor",
"indexBounds" : [ ],
"nscanned" : 64,
"nscannedObjects" : 64,
"n" : 64,
"millis" : 0,
"allPlans" : [
{
"cursor" : "BasicCursor"
"indexBounds" : [ ]
}
]
}
结果中的要点如下:
"cursor" : "BasicCursor"
这说明查询没有使用索引(因为没有查询条件)。
"nscanned" : 64
这个数字代表数据库查找了多少个文档。大家都想让这个数字尽可能地接近返回结果的数量。
"n" : 64
这个数字代表返回文档的数量。这个例子非常完美,因为扫描的文档数量和返回的文档数量完全一致。当然,这是因为反悔了整个集合,否则的话很难做到。
"millis" : 0
这个毫秒数表示数据库执行查询的时间。
假设现在有一个基于"age"键的索引,现在要查找20多岁的用户。对于这个查询使用explain:
> db.c.find({age : {$gt : 20, $lt : 30}}).explain()
{
"cursor" : "BtreeCursor age_1",
"indexBounds" : [
[
{
"age" : 20
},
{
"age" : 30
}
]
],
"nscaned" : 14,
"nscanedObjects" : 12,
"n" : 12,
"millis" : 1,
"allPlans" : [
{
"cursor" : "BtreeCursor age_1",
"indexBounds" : [
[
{
"age" : 20
},
{
"age" : 30
}
]
]
}
]
}
因为有了索引,不同于上例,explain输出的今个键值发生了改变:
"cursor" : "BtreeCursor age_1"
索引存储在B树的结构中,所以当使用索引查询,就会使用叫BtreeCursor类型的游标。
这个值也标识了使用的索引名age_1。通过这个名字,可以查询system.indexes集合来获取关于这个索引更进一步的信息(例如,是否是唯一索引,都包含那些键):
> db.system.indexes.find({"ns" : "test.c", "name" : "age_1"})
{
"_id" : ObjectId("4c0d211478b4eaaf7fb28565"),
"ns" : "test.c"
"key" : {
"age" : 1
},
"name" : "age_1"
}
"allPlans" : [ ... ]
这个键列举了所有MongoDB考虑的查询方案。
更复杂的索引的例子:假设已经有了{"username" : 1, "age" : 1}和{"age" : 1, "username" : 1}的索引了,现在要查询用户名和年龄:
> db.c.find({age : {$gt : 10}, username : "sally"}).explain()
如果发现MongoDB用了非预期的索引,可以用hint强制使用某个索引。例如,希望MongoDB在上一个例子中使用{"username" : 1, "age" : 1}索引,则需要:
> db.c.find({"age" : 14, "username" : /.*/}).hint({"username" : 1, "age" : 1})
『索引管理』
索引的原信息存储在每个数据库的system.indexes集合中。这是一个保留集合,不能对其插入或者删除文档。操作只能通过ensureIndex或者dropIndex进行。
system.indexes集合包含每个索引的详细信息,同时system.namespaces集合也含有索引的名字。如果查看这个集合,会发现每个集合至少有两个文档与之对应,一个对应集合本身,一个包含集合包含的索引。对于只有标准的"_id"索引的集合,system.namespaces应该累死这样:
{ "name" : "test.foo" }
{ "name" : "test.foo.$_id_" }
如果存在关于名字和年龄的符合索引,system.namespaces则会增加一条文档:
{ "name" : "test.foo.$name_1_age_1" }
『修改索引』
使用ensureIndex随时可以向现有几何添加新的索引:
> db.people.ensureIndex({"username" : 1}, {"background" : true})
使用{"background" : true}这个选项可以使连理索引的整个过程在后台完成,同时正常处理请求。
要是不适用background这个熏香,数据库会阻塞建立索引期间的所有请求。
使用dropIndex加上索引名将索引删除。
通常,要查一下system.indexes集合来找出索引名,因为即使是自动生成的名字也会因为驱动程序不同而不同。
> db.runCommand({"dropIndex" : "foo", "index" : "alphabet"})
要删除所有的索引,可以将index的值复位*:
> db.runCommand({"dropIndex" : "foo", "index" : "*"})
另外一种删除索引的方式就是删除集合。
『地理空间索引』
有一种查询变得越来越流行(尤其是移动设备的出现):找到离当前位置最近的N个场所。
MongoDB为坐标平面查询提供了专门的索引——地理空间索引。
假设要找到给定经纬度坐标周围最近的咖啡馆,就需要创建一个专门的索引来提高这种查询的效率,这是因为这种查询需要两个维度。地理空间索引可以由ensureIndex创建:
> db.map.ensureIndex({"gps" : "2d"})
这里的参数是"2d",而不是1或者-1。
"gps"键的值必须是某种形式的一对值:一个包含两个元素的数组或是包含两个键的内嵌文档。下面这些都是有效的:
{ "gps" : [ 0, 100 ] }
{ "gps" : { "x" : -30, "y" : 30 } }
{ "gps" : { "latitude" : -180, "longitude" : 180 } }
键名可以随意,例如{ "gps" : { "foo" : 0, "bar" : 1 } }也是可以的。
默认情况下,地理空间索引假设值的范围是-180~180(对经纬度来说很方便)。要是想用其他值,可以通过ensureIndex的选项来指定最大最小值:
> db.star.trek.ensureIndex({"lighf-years" : "2d"}, {"min" : 1000, "max" : 1000})
地理空间查询的两种方式:普通查询(用find)或者使用数据库命令。
find查询例子:
> db.map.find("gps" : {"$near" : [40, -73]})
这会按照离点(40,-73)由近及远的方式将map集合的所有文档都返回。
在没有使用limit时,默认返回100个文档。使用limit来限制返回的文档数:
> db.map.find({"gps" : {"$near" : [40, -73]}}).limit(10)
使用geoNear完成相同的操作:
> db.runCommand({geoNear : "map", near : [40, -73]}).limit(10)
geoNear还会返回每个文档到查询点的距离。
MongoDB不但能找到靠近一个点的文档,还能找到指定形状内的文档。做法是将"$near"换成"$within"。"$within"获取数量不断增加的形状作为参数,可以用来查找矩形和圆形内的所有点。
对于矩形,使用"$box"选项:
> db.map.find({"gps" : {"$within" : {"$box" : [[10, 20], [15, 30]]}}})
"$box"参数是两个元素的数组,第一个元素制定了左下角的坐标,第二个指定右上角的坐标。
对于圆形,使用"$center"选项:
> db.map.find({"gps" : {"$within" : {"$center" : [[12, 25], 5]}}})
复合地理空间索引
例:要查询"location"和"desc",就可以这样创建索引:
> db.ensureIndex({"location" : "2d", "desc" : 1})
然后就能很快找到最近的咖啡馆了:
> db.map.find({"location" : {"$near" : [-70, 30]}, "desc" : "coffeeshop"}).limit(1)