原文地址:http://pwhack.me/post/2014-06-25-1 转载注明出处数据库
本文摘录自《MongoDB权威指南》第八章,能够完全回答如下两个问题:segmentfault
数据表示的方式有不少种,其中最重要的问题之一就是在多大程度上对数据进行范式化。范式化(normalization)是将数据分散到多个不一样的集合,不一样集合之间能够相互引用数据。虽然不少文档能够引用某一块数据,可是这块数据只存储在一个集合中。因此,若是要修改这块数据,只需修改保存这块数据的那一个文档就好了。可是,MongoDB没有提供链接(join)工具,因此在不一样集合之间执行链接查询须要进行屡次查询。数组
反范式化(denormalization)与范式化相反:将每一个文档所需的数据都嵌入在文档内部。每一个文档都拥有本身的数据副本,而不是全部文档共同引用同一个数据副本。这意味着,若是信息发生了变化,那么全部相关文档都须要进行更新,可是在执行查询时,只须要一次查询,就能够获得全部数据。服务器
决定什么时候采用范式化什么时候采用反范式化时比较困难的。范式化可以提升数据写入速度,反范式化可以提升数据读取速度。须要根据本身应用程序的十几须要仔细权衡。网络
假设要保存学生和课程信息。一种表示方式是使用一个students集合(每一个学生是一个文档)和一个classes集合(每门课程是一个文档)。而后用第三个集合studentsClasses保存学生和课程之间的联系。工具
> db.studentsClasses.findOne({"studentsId": id}); { "_id": ObjectId("..."), "studentId": ObjectId("..."); "classes": [ ObjectId("..."), ObjectId("..."), ObjectId("..."), ObjectId("...") ] }
若是比较熟悉关系型数据库,可能你以前建国这种类型的表链接,虽然你的每一个记过文档中可能只有一个学生和一门课程(而不是一个课程“_id”列表)。将课程放在数组中,这有点儿MongoDB的风格,不过实际上一般不会这么保存数据,由于要经历不少次查询才能获得真实信息。post
假设要找到一个学生所选的课程。须要先查找students集合找到学生信息,而后查询studentClasses找到课程“_id”,最后再查询classes集合才能获得想要的信息。为了找出课程信息,须要向服务器请求三次查询。极可能你并不想再MongoDB中用这种数据组织方式,除非学生信息和课程信息常常发生变化,并且对数据读取速度也没有要求。优化
若是将课程引用嵌入在学生文档中,就能够节省一次查询:code
{ "_id": ObjectId("..."), "name": "John Doe", "classes": [ ObjectId("..."), ObjectId("..."), ObjectId("..."), ObjectId("...") ] }
"classes"字段是一个数组,其中保存了John Doe须要上的课程“_id”。须要找出这些课程的信息时,就可使用这些“_id”查询classes集合。这个过程只须要两次查询。若是数据不须要随时访问也不会随时发生变化(“随时”比“常常”要求更高),那么这种数据组织方式是很是好的。orm
若是须要进一步优化读取速度,能够将数据彻底反范式化,将课程信息做为内嵌文档保存到学生文档的“classes”字段中,这样只须要一次查询就能够获得学生的课程信息了:
{ "_id": ObjectId("..."), "name": "John Doe" "classes": [ { "class": "Trigonometry", "credites": 3, "room": "204" }, { "class": "Physics", "credites": 3, "room": "159" }, { "class": "Women in Literature", "credites": 3, "room": "14b" }, { "class": "AP European History", "credites": 4, "room": "321" } ] }
上面这种方式的优势是只须要一次查询就能够获得学生的课程信息,缺点是会占用更多的存储空间,并且数据同步更困难。例如,若是物理学的学分变成了4分(再也不是3分),那么选修了物理学课程的每一个学生文档都须要更新,并且不仅是更新“Physics”文档。
最后,也能够混合使用内嵌数据和引用数据:建立一个子文档数组用于保存经常使用信息,须要查询更详细信息时经过引用找到实际的文档:
{ "_id": ObjectId("..."), "name": "John Doe", "classes": [ { "_id": ObjectId("..."), "class": "Trigonometry" }, { "_id": ObjectId("..."), "class": "Physics" }, { "_id": ObjectId("..."), "class": "Women in Literature" }, { "_id": ObjectId("..."), "class": "AP European History" } ] }
这种方式也是不错的选择,由于内嵌的信息能够随着需求的变化进行修改,若是但愿在一个页面中包含更多(或者更少)的信息,就能够将更多(或者更少)的信息放在内嵌文档中。
须要考虑的另外一个重要问题是,信息更新更频繁仍是信息读取更频繁?若是这些数据会按期更新,那么范式化是比较好的选择。若是数据变化不频繁,为了优化更新效率儿牺牲读写速度就不值得了。
例如,教科书上介绍范式化的一个例子多是将用户和用户地址保存在不一样的集合中。可是,人们几乎不会改变住址,因此不该该为了这种几率极小的状况(某人改变了住址)而牺牲每一次查询的效率。在这种状况下,应该将地址内嵌在用户文档中。
若是决定使用内嵌文档,更新文档时,须要设置一个定时任务(cron job),以确保所作的每次更新都成功更新了全部文档。例如,咱们试图将更新扩散到多个文档,在更新完成全部文档以前,服务器崩溃了。须要可以检测到这种问题,而且从新进行未完的更新。
通常来讲,数据生成越频繁,就越不该该将这些内嵌到其余文档中。若是内嵌字段或者内嵌字段数量时无限增加的,那么应该将这些内容保存在单独的集合中,使用引用的方式进行访问,而不是内嵌到其余文档中,评论列表或者活动列表等信息应该保存在单独的集合中,不该该内嵌到其余文档中。
最后,若是某些字段是文档数据的一部分,那么须要将这些字段内嵌到文档中。若是在查询文档时常常须要将某个字段排除,那么这个字段应该放在另外的集合中,而不是内嵌在当前的文档中。
更适合内嵌 | 更适合引用 |
---|---|
子文档较小 | 子文档较大 |
数据不会按期改变 | 数据常常改变 |
最终数据一致便可 | 中间阶段的数据必须一致 |
文档数据小幅增长 | 文档数据大幅增长 |
数据一般须要执行二次查询才能得到 | 数据一般不包含在结果中 |
快速读取 | 快速写入 |
假如咱们有一个用户集合。下面是一些可能须要的字段,以及它们是否应该内嵌到用户文档中。
用户首选项只与特定用户相关,并且极可能须要与用户文档内的其余用户信息一块儿查询。因此用户首选项应该内嵌到用户文档中。
这个字段取决于最近活动增加和变化的频繁程度。若是这是个固定长度的字段(好比最近的10次活动),那么应该将这个字段内嵌到用户文档中。
一般不该该将好友信息内嵌到用户文档中,至少不该该将好友信息彻底内嵌到用户文档中。下节会介绍社交网络应用的相关内容。
不该该内嵌在用户文档中。
一个集合中包含的对其余集合的引用数量叫作基数(cardinality)。常见的关系有一对1、一对多、多对多。假若有一个博客应用程序。每篇博客文章(post)都有一个标题(title),这是一个对一个的关系。每一个做者(author)能够有多篇文章,这是一个对多的关系。每篇文章能够有多个标签(tag),每一个标签能够在多篇文章中使用,因此这是一个多对多的关系。
在MongoDB中,many(多)能够被分拆为两个子分类:many(多)和few(少)。假如,做者和文章之间多是一对少的关系:每一个做者只发表了为数很少的几篇文章。博客文章和标签多是多对少的关系:文章数量实际上极可能比标签数量多。博客文章和评论之间是一对多的关系:每篇文章能够拥有不少条评论。
只要肯定了少与多的关系,就能够比较容易地在内嵌数据和引用数据之间进行权衡。一般来讲,“少”的关系使用内嵌的方式会比较好,“多”的关系使用引用的方式比较好。
亲近朋友,远离敌人
不少社交类的应用程序都须要连接人、内容、粉丝、好友,以及其余一些事物。对于这些高度关联的数据使用内嵌的形式仍是引用的形式不容易权衡。这一节会介绍社交图谱数据相关的注意事项。一般,关注、好友或者收藏能够简化为一个发布、订阅系统:一个用户能够订阅另外一个用户相关的通知。这样,有两个基本操做须要比较高效:如何保存订阅者,如何将一个事件通知给全部订阅者。
比较常见的订阅实现方式有三种。第一种方式是将内容生产者内嵌在订阅者文档中:
{ "_id": ObjectId("..."), "username": "batman", "email": "batman@waynetech.com", "following": [ ObjectId("..."), ObjectId("...") ] }
如今,对于一个给定的用户文档,可使用形如db.activities.find({"user": {"$in": user["following"]}})
的方式查询该用户感兴趣的全部活动信息。可是,对于一条刚刚发布的活动信息,若是要找出对这条信息感兴趣的全部用户,就不得不查询全部用户的“following”字段了。
另外一种方式是将订阅者内嵌到生产者文档中:
{ "_id": ObjectId("..."), "username": "joker", "email": "joker@mailinator.com", "followers": [ ObjectId("..."), ObjectId("..."), ObjectId("...") ] }
当这个生产者新发布一条信息时,咱们当即就能够知道须要给哪些用户发布通知。这样作的缺点时,若是须要找到一个用户关注的用户列表,就必须查询整个用户集合。这样方式的优缺点与第一种方式的优缺点刚好相反。
同时,这两种方式都存在另外一个问题:它们会使用户文档变得愈来愈大,改变也愈来愈频繁。一般,“following”和“followers”字段甚至不须要返回:查询粉丝列表有多频繁?若是用户比较频繁地关注某些人或者对一些人取消关注,也会致使大量的碎片。所以,最后的方案对数据进一步范式化,将订阅信息保存在单独的集合中,以免这些缺点。进行这种成都的范式化可能有点儿过了,可是对于常常发生变化并且不须要与文档其余字段一块儿返回的字段,这很是有用。对“followers”字段作这种范式化使有意义的。
用一个集合来保存发布者和订阅者的关系,其中的文档结构可能以下所示:
{ "_id": ObjectId("..."), //被关注者的"_id" "followers": [ ObjectId("..."), ObjectId("..."), ObjectId("...") ] }
这样可使用户文档比较精简,可是须要额外的查询才能获得粉丝列表。因为“followers”数组的大小常常会发生变化,因此能够在这个集合上启用“usePowerOf2Sizes”,以保证users集合尽量小。若是将followers集合保存在另外一个数据库中,也能够在不过多影响users集合的前提下对其进行压缩。
无论使用什么样的策略,内嵌字段只能在子文档或者引用数量不是特别大的状况下有效发挥做用。对于比较有名的用户,可能会致使用于保存粉丝列表的文档溢出。对于这种状况的一种解决方案使在必要时使用“连续的”文档。例如:
> db.users.find({"username": "wil"}) { "_id": ObjectId("..."), "username": "wil", "email": "wil@example.com", "tbc": [ ObjectId("123"), // just for example ObjectId("456") // same as above ], "followers": [ ObjectId("..."), ObjectId("..."), ObjectId("..."), ... ] } { "_id": ObjectId("123"), "followers": [ ObjectId("..."), ObjectId("..."), ObjectId("..."), ... ] } { "_id": ObjectId("456"), "followers": [ ObjectId("..."), ObjectId("..."), ObjectId("..."), ... ] }
对于这种状况,须要在应用程序中添加从“tbc”(to be continued)数组中取数据的相关逻辑。
No silver bullet.