MongoDB 倾向于将数据都放在一个 Collection 下吗?
不是这样的。
Collection 的单个 doc 有大小上限,现在是 16MB,这就使得你不可能把所有东西都揉到一个 collection 里。而且如果 collection 结构过于复杂,既会影响查询、更新效率,也会造成维护困难和操作风险。你有尝试过手一抖就把一个 doc 不小心存成 null 的么,反正我做过,要是一个人所有信息都在这个 collection 里面,那感觉一定相当酸爽吧。
一般的原则是:
- 按照查询方式来聚类
- 需要经常一起读取的数据放一起.
- 在逻辑上关系紧密的信息放在一起。
- 有 map-reduce/aggregation 需求的数据放在一起,这些操作都只能操作单个 collection。
- 按照数据量来拆分
- 如果发现要在 collection 里面用数组,数组长度还会不断增加,那么应该把数据内容放到一个专门的 collection,每条数据都引用当前这个 doc 的主键(就像 mysql 的 1..N 外键依赖一样)。
- 如果发现某个 doc 层次过深(超过 2 层),八成得考虑要拆分了,要不然性能和可维护性都会有问题。
- 按照有表结构的方式来设计
- MongoDB 是没有表结构这个概念的,但是实际使用的时候,很少说一个 collection 里面存在各式各样结构的 doc,如果发现 doc 的结构差别越来越大了,那么应该考虑怎么抽象成类似结构,把变化的东西扔到其他 collection 去,用外键依赖的方式互相引用。
比如设计一个用户系统,user collection 应该放 name 等常用的信息,也应该放 lastLoginAt 这些仅跟 user 相关的东西,或许应该把用户有哪些访问权限的信息也放进来,但是不要放用户的登录日志这种信息会不断增加的信息。
至于 user 之间的关系是否存在 user collection 则需要讨论。假如仅仅需要存储用户间的关系,记录下好友的 uid 就行,而且好友数量也不太大,几百个最多了,那么我倾向于放在一个 collection 里。如果关系数据本身就比较复杂,或者好友数会上千,那我倾向于拆分。
另外,Mongodb 官方的 数据模型设计范式 很值得一读,推荐去好好看看。
原文地址:http://pwhack.me/post/2014-06-25-1 转载注明出处
本文摘录自《MongoDB权威指南》第八章,可以彻底回答以下两个问题:
数据表示的方式有很多种,其中最重要的问题之一就是在多大程度上对数据进行范式化。范式化(normalization)是将数据分散到多个不同的集合,不同集合之间可以相互引用数据。虽然很多文档可以引用某一块数据,但是这块数据只存储在一个集合中。所以,如果要修改这块数据,只需修改保存这块数据的那一个文档就行了。但是,MongoDB没有提供连接(join)工具,所以在不同集合之间执行连接查询需要进行多次查询。
反范式化(denormalization)与范式化相反:将每个文档所需的数据都嵌入在文档内部。每个文档都拥有自己的数据副本,而不是所有文档共同引用同一个数据副本。这意味着,如果信息发生了变化,那么所有相关文档都需要进行更新,但是在执行查询时,只需要一次查询,就可以得到所有数据。
决定何时采用范式化何时采用反范式化时比较困难的。范式化能够提高数据写入速度,反范式化能够提高数据读取速度。需要根据自己应用程序的十几需要仔细权衡。
数据表示的例子
假设要保存学生和课程信息。一种表示方式是使用一个students集合(每个学生是一个文档)和一个classes集合(每门课程是一个文档)。然后用第三个集合studentsClasses保存学生和课程之间的联系。
> db.studentsClasses.findOne({"studentsId": id});
{
"_id": ObjectId("..."),
"studentId": ObjectId("...");
"classes": [
ObjectId("..."),
ObjectId("..."),
ObjectId("..."),
ObjectId("...")
]
}
如果比较熟悉关系型数据库,可能你之前建国这种类型的表连接,虽然你的每个记过文档中可能只有一个学生和一门课程(而不是一个课程“_id”列表)。将课程放在数组中,这有点儿MongoDB的风格,不过实际上通常不会这么保存数据,因为要经历很多次查询才能得到真实信息。
假设要找到一个学生所选的课程。需要先查找students集合找到学生信息,然后查询studentClasses找到课程“_id”,最后再查询classes集合才能得到想要的信息。为了找出课程信息,需要向服务器请求三次查询。很可能你并不想再MongoDB中用这种数据组织方式,除非学生信息和课程信息经常发生变化,而且对数据读取速度也没有要求。
如果将课程引用嵌入在学生文档中,就可以节省一次查询:
{
"_id": ObjectId("..."),
"name": "John Doe",
"classes": [
ObjectId("..."),
ObjectId("..."),
ObjectId("..."),
ObjectId("...")
]
}
"classes"字段是一个数组,其中保存了John Doe需要上的课程“_id”。需要找出这些课程的信息时,就可以使用这些“_id”查询classes集合。这个过程只需要两次查询。如果数据不需要随时访问也不会随时发生变化(“随时”比“经常”要求更高),那么这种数据组织方式是非常好的。
如果需要进一步优化读取速度,可以将数据完全反范式化,将课程信息作为内嵌文档保存到学生文档的“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),以确保所做的每次更新都成功更新了所有文档。例如,我们试图将更新扩散到多个文档,在更新完成所有文档之前,服务器崩溃了。需要能够检测到这种问题,并且重新进行未完的更新。
一般来说,数据生成越频繁,就越不应该将这些内嵌到其他文档中。如果内嵌字段或者内嵌字段数量时无限增长的,那么应该将这些内容保存在单独的集合中,使用引用的方式进行访问,而不是内嵌到其他文档中,评论列表或者活动列表等信息应该保存在单独的集合中,不应该内嵌到其他文档中。
最后,如果某些字段是文档数据的一部分,那么需要将这些字段内嵌到文档中。如果在查询文档时经常需要将某个字段排除,那么这个字段应该放在另外的集合中,而不是内嵌在当前的文档中。
更适合内嵌 | 更适合引用 |
---|---|
子文档较小 | 子文档较大 |
数据不会定期改变 | 数据经常改变 |
最终数据一致即可 | 中间阶段的数据必须一致 |
文档数据小幅增加 | 文档数据大幅增加 |
数据通常需要执行二次查询才能获得 | 数据通常不包含在结果中 |
快速读取 | 快速写入 |
假如我们有一个用户集合。下面是一些可能需要的字段,以及它们是否应该内嵌到用户文档中。
用户首选项(account preferences)
用户首选项只与特定用户相关,而且很可能需要与用户文档内的其他用户信息一起查询。所以用户首选项应该内嵌到用户文档中。
最近活动(recent activity)
这个字段取决于最近活动增长和变化的频繁程度。如果这是个固定长度的字段(比如最近的10次活动),那么应该将这个字段内嵌到用户文档中。
好友(friends)
通常不应该将好友信息内嵌到用户文档中,至少不应该将好友信息完全内嵌到用户文档中。下节会介绍社交网络应用的相关内容。
所有由用户产生的内容
不应该内嵌在用户文档中。
基数
一个集合中包含的对其他集合的引用数量叫做基数(cardinality)。常见的关系有一对一、一对多、多对多。假如有一个博客应用程序。每篇博客文章(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.
如果业务总是需要查询用户间关系 不如还是把关系独立一个Collection
https://segmentfault.com/q/1010000000589390
MongoDB 倾向于将数据都放在一个 Collection 下吗?的更多相关文章
- 百万级运维心得一:Mongodb和Redis数据不能放在同一个服务器
百万级运维经验一:Mongodb和Redis数据不能放在同一个服务器 一开始时,为了省服务器,把Mongodb和Redis放在一个服务器上.网站每到高峰期都特别卡,还经常出现502.找了很久的原因,发 ...
- MongoDB 复制一个collection里的数据到另一个collection
mongodb shell 中执行: db.source(复制源表).find().forEach(function(x){ db.target(目的表).insert(x); })
- 如何对多个文件进行MODELSIM仿真? (由于是一个很大的项目,不可能把所有MODULE都放在一个文件里。 如何在ModelSim中对多个.V文件进行仿真?)
可以将所有要编译的所有文件的名字做一个list.新建一个文本文档,重命名为vflist vflist内容例子如下(src为文件夹):src/base_addr_chk.vsrc/config_mux. ...
- 关于.net 中Clipboard.GetDataObject() 之后读出数据读出的数据都是相同的解决方法
模拟键盘sendkey("^c") 多次复制之后 当使用Clipboard.GetDataObject() 读出数据都是一个值 经过多次尝试 提供一个解决方案 IDataObjec ...
- 你需要一个新的model实体的时候必须new一个.奇怪的问题: 使用poi解析Excel的把数据插入数据库同时把数据放在一个list中,返回到页面展示,结果页面把最后一条数据显示了N次
数据库显示数据正常被插 插入一条打印一次数据,也是正常的,但是执行完,list就全部变成了最后一条数据.很奇怪 单步调试 给list插入第一条数据 model是6607 连续插了多条数据都是6607 ...
- Excel表格中依据某一列的值,将这列中一样的数据放在一个文件中。
一需求:按照标题C的内容,一样的数据整理到一个文件中. 二.操作: 1.atl+F11弹出vb窗口 2.点击 插入===>模块 ,复制以下代码,注意这是一个表头为三行的函数(保存 ...
- 13.mutiset树每一个结点都是一个链表的指针,可以存储相同的数据
#include <iostream> //红黑树(自动保证平衡,自动生成平衡查找树) #include <set> #include <cstring> #inc ...
- MongoDB中的数据聚合工具Aggregate和Group
周煦辰 2016-01-16 来说说MongoDB中的数据聚合工具. Aggregate是MongoDB提供的众多工具中的比较重要的一个,类似于SQL语句中的GROUP BY.聚合工具可以让开发人员直 ...
- 【云计算】K8S DaemonSet 每个node上都运行一个pod
Kubernetes容器集群中的日志系统集成实践 Kubernetes是原生的容器编排管理系统,对于负载均衡.服务发现.高可用.滚动升级.自动伸缩等容器云平台的功能要求有原生支持.今天我分享一下我们在 ...
随机推荐
- CSS-文本(中,英)
1.缩进文本:text-indent 2.水平对齐:text-align: left/center/right/justify(实现两端对齐文本效果) 3.字间隔:word-spacing(可以改变 ...
- 搭建vue-cli时候报错处理
最近在使用vue-cli搭建目录时出现 “webpack-dev-server不是内部或外部命令,也不是可运行的程序 或批处理文件”情况 在网上查了不少资料和解决方法后,结合自己的情况应该是环境变量的 ...
- Codeforces 919 B. Perfect Number
B. Perfect Number time limit per test 2 seconds memory limit per test 256 megabytes input standa ...
- POJ 1797 Heavy Transportation SPFA变形
原题链接:http://poj.org/problem?id=1797 Heavy Transportation Time Limit: 3000MS Memory Limit: 30000K T ...
- 期望DP初步
感觉期望DP这种东西像是玄学- 主要总结说一点基础性的东西, 或许对于理解题目的做法会有一点帮助. 首先是关于独立事件, 互斥事件的概念. 通俗地说, 就是对于两个事件A, B, 假如满足发生了其中一 ...
- 346. Moving Average from Data Stream
/* * 346. Moving Average from Data Stream * 2016-7-11 by Mingyang * 这里注意的就是(double) sum / count * su ...
- Go -- 接口赋值
在go语言中,接口赋值分为2中情况: 1.将对象实例赋值给接口: 2.将一个接口赋值给另一个接口. 1.将对象实例赋值给接口: 要求对象实现了接口的所有方法. 2.将接口赋值给另一个接口: 假设接口A ...
- NN优化方法对照:梯度下降、随机梯度下降和批量梯度下降
1.前言 这几种方法呢都是在求最优解中常常出现的方法,主要是应用迭代的思想来逼近.在梯度下降算法中.都是环绕下面这个式子展开: 当中在上面的式子中hθ(x)代表.输入为x的时候的其当时θ參数下的输出值 ...
- Swift----编程语言语法
1 简单介绍 今天凌晨Apple刚刚公布了Swift编程语言,本文从其公布的书籍<The Swift Programming Language>中摘录和提取而成.希望对各位的iOS&a ...
- UVA - 11354Bond最小生成树,LCA寻找近期公共祖先
看懂题目意思.他的意思是求将全部的城市走一遍,危急度最小.而且给 你两个s,t后让你求在走的时候,从s到t过程中危急度最大的值,并输出它, 然后就是怎样攻克了,这个题目能够说简单,也能够说难 通过思考 ...