上一篇文章: MongoDB指南---1三、索引类型、索引管理
下一篇文章: MongoDB指南---1五、特殊的索引和集合:地理空间索引、使用GridFS存储文件
本章介绍MongoDB中一些特殊的集合和索引类型,包括:git
MongoDB中的“普通”集合是动态建立的,并且能够自动增加以容纳更多的数据。MongoDB中还有另外一种不一样类型的集合,叫作固定集合,固定集合须要事先建立好,并且它的大小是固定的(如图6-1所示)。说到固定大小的集合,有一个颇有趣的问题:向一个已经满了的固定集合中插入数据会怎么样?答案是,固定集合的行为相似于循环队列。若是已经没有空间了,最老的文档会被删除以释放空间,新插入的文档会占据这块空间(如图6-2所示)。也就是说,当固定集合被占满时,若是再插入新文档,固定集合会自动将最老的文档从集合中删除。github
图6-1 新文档被插入到队列末尾正则表达式
图6-2 若是队列已经被占满,那么最老的文档会被以后插入的新文档覆盖
固定集合的访问模式与MongoDB中的大部分集合不一样:数据被顺序写入磁盘上的固定空间。所以它们在碟式磁盘(spinning disk)上的写入速度很是快,尤为是集合拥有专用磁盘时(这样就不会由于其余集合的一些随机性的写操做而“中断”)。shell
固定集合不能被分片。
固定集合能够用于记录日志,尽管它们不够灵活。虽然能够在建立时指定集合大小,但没法控制何时数据会被覆盖。segmentfault
不一样于普通集合,固定集合必须在使用以前先显式建立。可使用create命令建立固定集合。在shell中,可使用createCollection函数:api
> db.createCollection("my_collection", {"capped" : true, "size" : 100000}); { "ok" : true }
上面的命令建立了一个名为my_collection大小为100 000字节的固定集合。
除了大小,createCollection还可以指定固定集合中文档的数量:数组
> db.createCollection("my_collection2", ... {"capped" : true, "size" : 100000, "max" : 100}); { "ok" : true }
可使用这种方式来保存最新的10则新闻,或者是将每一个用户的文档数量限制为1000。
固定集合建立以后,就不能改变了(若是须要修改固定集合的属性,只能将它删除以后再重建)。所以,在建立大的固定集合以前应该仔细想清楚它的大小。缓存
为固定集合指定文档数量限制时,必须同时指定固定集合的大小。无论先达到哪个限制,以后插入的新文档就会把最老的文档挤出集合:固定集合的文档数量不能超过文档数量限制,固定集合的大小也不能超过大小限制。
建立固定集合时还有另外一个选项,能够将已有的某个常规集合转换为固定集合,可使用convertToCapped命令实现。下面的例子将test集合转换为一个大小为10 000字节的固定集合:服务器
> db.runCommand({"convertToCapped" : "test", "size" : 10000}); { "ok" : true }
没法将固定集合转换为非固定集合(只能将其删除)。app
对固定集合能够进行一种特殊的排序,称为天然排序(natural sort)。天然排序返回结果集中文档的顺序就是文档在磁盘上的顺序(如图6-3所示)。
图6-3 使用{"$natural" : 1}进行排序
对大多数集合来讲,天然排序的意义不大,由于文档的位置常常变更。可是,固定集合中的文档是按照文档被插入的顺序保存的,天然顺序就是文档的插入顺序。所以,天然排序获得的文档是从旧到新排列的。固然也能够按照重新到旧的顺序排列(如图6-4所示)。
> db.my_collection.find().sort({"$natural" : -1})
图6-4 使用{"$natural" : -1}进行排序
循环游标(tailable cursor)是一种特殊的游标,当循环游标的结果集被取光后,游标不会被关闭。循环游标的灵感来自tail -f命令(循环游标跟这个命令有点儿类似),会尽量久地持续提取输出结果。因为循环游标在结果集取光以后不会被关闭,所以,当有新文档插入到集合中时,循环游标会继续取到结果。因为普通集合并不维护文档的插入顺序,因此循环游标只能用在固定集合上。
循环游标一般用于当文档被插入到“工做队列”(其实就是个固定集合)时对新插入的文档进行处理。若是超过10分钟没有新的结果,循环游标就会被释放,所以,当游标被关闭时自动从新执行查询是很是重要的。下面是一个在PHP中使用循环游标的例子(不能在mongo shell中使用循环游标):
$cursor = $collection->find()->tailable(); while (true) { if (!$cursor->hasNext()) { if ($cursor->dead()) { break; } sleep(1); } else { while ($cursor->hasNext()) { do_stuff($cursor->getNext()); } } }
这个游标会不断对查询结果进行处理,或者是等待新的查询结果,直到游标被关闭(超过10分钟没有新的结果或者人为停止查询操做)。
默认状况下,每一个集合都有一个"_id"索引。可是,若是在调用createCollection建立集合时指定autoIndexId选项为false,建立集合时就不会自动在"_id"上建立索引。实践中不建议这么使用,可是对于只有插入操做的集合来讲,这确实能够带来速度的稍许提高。
若是建立了一个没有"_id"索引的集合,那就永远都不能复制它所在的mongod了。复制操做要求每一个集合上都要有"_id"索引(对于复制操做,可以惟一标识集合中的每个文档是很是重要的)。
在2.2版本以前,固定集合默认是没有"_id"索引的,除非显式地将autoIndexId置为true。若是正在使用旧版的固定集合,要确保你的应用程序可以填充"_id"字段(大多数驱动程序会自动填充"_id"字段),而后使用ensureIndex命令建立"_id"索引。
记住,"_id"索引必须是惟一索引。不一样于其余索引,"_id"索引一经建立就没法删除了,所以在生产环境中建立索引以前先本身实践一下是很是重要的。因此建立"_id"索引必须一次成功!若是建立的"_id"索引不合规范,就只能删除集合再重建了。
上一节已经讲过,对于固定集合中的内容什么时候被覆盖,你只拥有很是有限的控制权限。若是须要更加灵活的老化移出系统(age-out system),可使用TTL索引(time-to-live index,具备生命周期的索引),这种索引容许为每个文档设置一个超时时间。一个文档到达预设置的老化程度以后就会被删除。这种类型的索引对于缓存问题(好比会话的保存)很是有用。
在ensureIndex中指定expireAfterSecs选项就能够建立一个TTL索引:
> // 超时时间为24小时 > db.foo.ensureIndex({"lastUpdated" : 1}, {"expireAfterSecs" : 60*60*24})
这样就在"lastUpdated"字段上创建了一个TTL索引。若是一个文档的"lastUpdated"字段存在而且它的值是日期类型,当服务器时间比文档的"lastUpdated"字段的时间晚expireAfterSecs秒时,文档就会被删除。
为了防止活跃的会话被删除,能够在会话上有活动发生时将"lastUpdated"字段的值更新为当前时间。只要"lastUpdated"的时间距离当前时间达到24小时,相应的文档就会被删除。
MongoDB每分钟对TTL索引进行一次清理,因此不该该依赖以秒为单位的时间保证索引的存活状态。可使用collMod命令修改expireAfterSecs的值:
> db.runCommand({"collMod" : "someapp.cache", "expireAfterSecs" : 3600})
在一个给定的集合上能够有多个TTL索引。TTL索引不能是复合索引,可是能够像“普通”索引同样用来优化排序和查询。
MongoDB有一个特殊类型的索引用于在文档中搜索文本。前面几章都是使用精确匹配和正则表达式来查询字符串,可是这些技术有一些限制。使用正则表达式搜索大块文本的速度很是慢,并且没法处理语言的理解问题(好比entry与entries应该算是匹配的)。使用全文本索引能够很是快地进行文本搜索,就如同内置了多种语言分词机制的支持同样。
建立任何一种索引的开销都比较大,而建立全文本索引的成本更高。在一个操做频繁的集合上建立全文本索引可能会致使MongoDB过载,因此应该是离线状态下建立全文本索引,或者是在对性能没要求时。建立全文本索引时要特别当心谨慎,内存可能会不够用(除非你有SSD)。
全文本索引也会致使比“普通”索引更严重的性能问题,由于全部字符串都须要被分解、分词,而且保存到一些地方。所以,可能会发现拥有全文本索引的集合的写入性能比其余集合要差。全文本索引也会下降分片时的数据迁移速度:将数据迁移到其余分片时,全部文本都须要从新进行索引。
写做本书时,全文本索引仍然只是一个处于“试验阶段”的功能,因此须要专门启用这个功能才能进行使用。启动MongoDB时指定--setParameter textSearch Enabled=true选项,或者在运行时执行setParameter命令,均可以启用全文本索引:
> db.adminCommand({"setParameter" : 1, "textSearchEnabled" : true})
假如咱们使用这个非官方的Hacker News JSON API(http://api.ihackernews.com)将最近的一些文章加载到了MongoDB中。
为了进行文本搜索,首先须要建立一个"text"索引:
> db.hn.ensureIndex({"title" : "text"})
如今,必须经过text命令才能使用这个索引(写做本书时,全文本索引还不能用在“普通”查询中):
test> db.runCommand({"text" : "hn", "search" : "ask hn"}) { "queryDebugString" : "ask|hn||||||", "language" : "english", "results" : [ { "score" : 2.25, "obj" : { "_id" : ObjectId("50dcab296803fa7e4f000011"), "title" : "Ask HN: Most valuable skills you have?", "url" : "/comments/4974230", "id" : 4974230, "commentCount" : 37, "points" : 31, "postedAgo" : "2 hours ago", "postedBy" : "bavidar" } }, { "score" : 0.5625, "obj" : { "_id" : ObjectId("50dcab296803fa7e4f000001"), "title" : "Show HN: How I turned an old book...", "url" : "http://www.howacarworks.com/about", "id" : 4974055, "commentCount" : 44, "points" : 95, "postedAgo" : "2 hours ago", "postedBy" : "AlexMuir" } }, { "score" : 0.5555555555555556, "obj" : { "_id" : ObjectId("50dcab296803fa7e4f000010"), "title" : "Show HN: ShotBlocker - iOS Screenshot detector...", "url" : "https://github.com/clayallsopp/ShotBlocker", "id" : 4973909, "commentCount" : 10, "points" : 17, "postedAgo" : "3 hours ago", "postedBy" : "10char" } } ], "stats" : { "nscanned" : 4, "nscannedObjects" : 0, "n" : 3, "timeMicros" : 89 }, "ok" : 1 }
匹配到的文档是按照相关性降序排列的:"Ask HN"位于第一位,而后是两个部分匹配的文档。每一个对象前面的"score"字段描述了每一个结果与查询的匹配程度。
如你所见,这个搜索是不区分大小写不的,至少对于[a-zA-Z]这些字符是这样。全文本索引会使用toLower将单词变为小写,但这是与本地化相关的,因此某些语言的用户可能会发现MongoDB会不可预测性地变得区分大小写,这取决于toLower在不一样字符集上的行为。MongoDB一直在努力提升对不一样字符集的支持。
全文本索引只会对字符串数据进行索引:其余的数据类型会被忽略,不会包含在索引中。一个集合上最多只能有一个全文本索引,可是全文本索引能够包含多个字段:
> db.blobs.ensureIndex({"title" : "text", "desc" : "text", "author" : "text"})
与“普通”的多键索引不一样,全文本索引中的字段顺序不重要:每一个字段都被同等对待。能够为每一个字段指定不一样的权重来控制不一样字段的相对重要性:
> db.hn.ensureIndex({"title" : "text", "desc" : "text", "author" : "text"}, ... {"weights" : {"title" : 3, "author" : 2}})
默认的权重是1,权重的范围能够是1~1 000 000 000。使用上面的代码设置权重以后,"title"字段成为其中最重要的字段,"author"其次,最后是"desc"(没有指定,所以它的权重是默认值1)。
索引一经建立,就不能改变字段的权重了(除非删除索引再重建),因此在生产环境中建立索引以前应该先在测试数据集上实际操做一下。
对于某些集合,咱们可能并不知道每一个文档所包含的字段。可使用"$**"在文档的全部字符串字段上建立全文本索引:这不只会对顶级的字符串字段创建索引,也会搜索嵌套文档和数组中的字符串字段:
> db.blobs.ensureIndex({"$**" : "text"})
也能够为"$**"设置权重:
> db.hn.ensureIndex({"whatever" : "text"}, ... {"weights" : {"title" : 3, "author" : 1, "$**" : 2}})
"whatever"能够指代任何东西。在设置权重时指明了是对全部字段进行索引,所以MongoDB并不要求你明确给出字段列表。
默认状况下,MongoDB会使用OR链接查询中的每一个词:“ask OR hn”。这是执行全文本查询最有效的方式,可是也能够进行短语的精确匹配,以及使用NOT。为了精确查询“ask hn”这个短语,能够用双引号将查询内容括起来:
> db.runCommand({text: "hn", search: "\"ask hn\""}) { "queryDebugString" : "ask|hn||||ask hn||", "language" : "english", "results" : [ { "score" : 2.25, "obj" : { "_id" : ObjectId("50dcab296803fa7e4f000011"), "title" : "Ask HN: Most valuable skills you have?", "url" : "/comments/4974230", "id" : 4974230, "commentCount" : 37, "points" : 31, "postedAgo" : "2 hours ago", "postedBy" : "bavidar" } } ], "stats" : { "nscanned" : 4, "nscannedObjects" : 0, "n" : 1, "nfound" : 1, "timeMicros" : 20392 }, "ok" : 1 }
这比使用OR的匹配慢一些,由于MongoDB首先要执行一个OR匹配,而后再对匹配结果进行AND匹配。
能够将查询字符串的一部分指定为字面量匹配,另外一部分仍然是普通匹配:
> db.runCommand({text: "hn", search: "\"ask hn\" ipod"})
这会精确搜索"ask hn"这个短语,也会可选地搜索"ipod"。
也可使用"-"字符指定特定的词不要出如今搜索结果中:
> db.runCommand({text: "hn", search: "-startup vc"})
这样就会返回匹配“vc”可是不包含“startup”这个词的文档。
有几种方式能够优化全文本搜索。若是可以使用某些查询条件将搜索结果的范围变小,能够建立一个由其余查询条件前缀和全文本字段组成的复合索引:
> db.blog.ensureIndex({"date" : 1, "post" : "text"})
这就是局部的全文本索引,MongoDB会基于上面例子中的"date"先将搜索范围分散为多个比较小的树。这样,对于特定日期的文档进行全文本查询就会快不少了。
也可使用其余查询条件后缀,使索引可以覆盖查询。例如,若是要返回"author"和"post"字段,能够基于这两个字段建立一个复合索引:
> db.blog.ensureIndex({"post" : "text", "author" : 1})
前缀和后缀形式也能够组合在一块儿使用:
> db.blog.ensureIndex({"date" : 1, "post" : "text", "author" : 1})
这里的前缀索引字段和后缀索引字段都不能够是多键字段。
建立全文本索引会自动在集合上启用usePowerOf2Sizes选项,这个选项能够控制空间的分配方式。这个选项可以提升写入速度,因此不要禁用它。
当一个文档被插入以后(或者索引第一次被建立以后),MongoDB会查找索引字段,对字符串进行分词,将其减少为一个基本单元(essential unit)。而后,不一样语言的分词机制是不一样的,因此必须指定索引或者文档使用的语言。文本类型的索引容许指定"default_language"选项,它的默认值是"english",能够被设置为多种其余语言(MongoDB的在线文档提供了最新的支持语言列表)。
例如,要建立一个法语的索引,能够这么作:
> db.users.ensureIndex({"profil" : "text", "intérêts" : "text"}, ... {"default_language" : "french"})
这样,这个索引就会默认使用法语的分词机制,除非指定了其余的分词机制。若是在插入文档时指定"language"字段,就能够为每一个文档分别指定分词时使用的语言:
> db.users.insert({"username" : "swedishChef", ... "profile" : "Bork de bork", language : "swedish"})
上一篇文章: MongoDB指南---1三、索引类型、索引管理
下一篇文章: MongoDB指南---1五、特殊的索引和集合:地理空间索引、使用GridFS存储文件