Document-Oriented StorageJSON-style documents with dynamic schemas offer simplicity and power.
面向文档存储
的文件与动态模式使MongoDB简单而强大。
Schema Design:模式设计
在MongoDB中Schema的设计是非常不同于传统的DBMS。然而Schema是非常重要的,并且是建立应用程序的第一步。
在传统的数据模型中,给一个实体关系模型一个独立的用例在概念上是正确的,这是一个很正常的第三范式,但这通常会偏离你处于性能方面的考虑。在MongoDB中,Schema的设计不仅仅是对数据进行建模的用例。根据最常见的用例,我们对Schema的设计进行了优化,这有利有弊——用例通常是高性能的。然而有一个偏见是说Schema可能使某些动态查询相比于关系模型缺少一点优雅。
当我们要设计Schema时,需要考虑以下问题:
1.什么时候我们嵌入数据和链接(见下文)?我们在这里的决定讲影响第二个问题的答案
2.我们有多少集合,它们是什么?
3.什么时候我们需要原子操作?这些操作可以执行范围内的BSON文档,但并不是所有文档。
4.我们将创建什么索引使查询和更新快?
5.我们如何切分?什么是分片键?
Embedding and Linking:嵌入和链接 |
在设计一个MongoDB Schema时一个关键问题是什么时候嵌入,什么时候链接。嵌入是嵌套对象和数组到BSON文档中,链接是文档之间的引用。
在MongoDB中没有join——在1000服务器集群中做分布式join是很困难的。嵌入有点像“prejoined”(预连接)数据。
服务器处理在一个文档里面的操作是很容易的,这些操作可以相当丰富。链接相比之下必须处理客户端应用程序,应用程序是通过发行一个后续查询来处理文档。
一般来说,实体之间有“包含”关系,则应该选择嵌入。当不使用连接会导致重复的数据,那么就选择使用链接。
Collections:集合 |
在MongoDB中集合类似于关系数据库中的表,每一个集合包含文档,正如上面提到的这些文件可以相当丰富。在一个集合文档内字段是没有显式声明。然而来自于Schema设计师的一个关于那些字段将会是什么的概念,并且文档在集合内是怎样被结构化的。MongoDB不需要集合内的文档有相同的结构,然而在实践中大多数集合都是高度同质的。只要我们愿意我们就可以避免这些,例如当添加一个新字段,在这种情况一个“alter table”风格操作不是必要的。
Atomic Operations:原子操作 |
有些问题需要能够执行原子操作。例如,简单地增加计数器一个需要的原子性操作的案例。MongoDB还可以执行更复杂的操作,如下面所示的伪代码:
atomically { if( doc.credits > 5 ) { doc.credits -= 5; doc.debits += 5; }}
另一个例子是一个用户注册的场景,我们永远不会想要两个用户注册相同的用户名同时:
atomically { if( exists a document with username='jane' ) { print "username already in use please choose another"; } else { insert a document with username='jane' in the users collection; print("thanks you have registered as user jane."); }}
Schema设计的关键点是我们的范围的原子性/ ACID属性就是文档。
Indexes:索引 |
MongoDB支持声明的索引。在MongoDB中索引非常类似于关系数据库中的索引:它们都需要有效的查询处理,必须显式声明。因此我们需要考虑我们将定义哪些索引作为Schema的一部分设计过程。就像在关系数据库,索引可以在以后添加——如果我们决定以后有一个新的索引,我们可以这样做。
Sharding:分片 |
另一个考虑事项是分片模式设计。一个BSON文档(可能有大量的嵌入)驻留在一个且只有一个分片中。
一个集合可能包含分片。当分片时,集合有一个分片键,这决定了集合是如何在分片中分割的。通常(但不总是)查询一个分片集合包含作为查询表达式的一部分的分片键。
这里的关键是,改变分片键是困难的,从一开始你就会想要选择。
Example:示例 |
让我们考虑一个例子,这是一个内容管理系统。下面的例子使用mongo shell语法,当然也可以使用其他任何编程语言来做——只要选择语言的相关驱动就可以。
我们的内容管理系统会有新帖子,文章的作者。我们想支持评论和投票帖子,我们也将提供对帖子的索引查询。
对于这个场景一个比较好的schema设计是建立两个MongoDB集合,一个是帖子集合另一个是用户集合,这就是我们将使用的示例。
我们的用户有几个属性——一个用户id注册的、他们的真实姓名、和他们的报酬,例如我们可以执行以下命令:
>db.users.insert( { _id : "alex", name: { first:"Alex", last:"Benisson" }, karma : 1.0 } )
这个_id字段总是存在MongoDB中,是用一种独特的automically索引键约束。这是适合我们的用户名,所以我们将它们存储在_id字段。我们不需要,但我们可以设置一个独立的用户名字段,并让系统automically生成一个惟一的id。
现在让我们考虑帖子,我们将假设一些帖子已经填充,让我们查询一个:
> db.posts.findOne(){ _id : ObjectId("4e77bb3b8a3e000000004f7a"), when : Date("2011-09-19T02:10:11.3Z", author : "alex", title : "No Free Lunch", text : "This is the text of the post. It could be very long.", tags : [ "business", "ramblings" ], votes : 5, voters : [ "jane", "joe", "spencer", "phyllis", "li" ], comments : [ { who : "jane", when : Date("2011-09-19T04:00:10.112Z"), comment : "I agree." }, { who : "meghan", when : Date("2011-09-20T14:36:06.958Z"), comment : "You must be joking. etc etc ..." } ]}
有趣的是,与之形成对比的是,我们如何在一个关系数据库设计相同的schema。我们可能会有一个用户集合和一个帖子集合,但是另外通常会有一个标签集合,一个投票集合和评论集合。
我们可以用一个简单的语句在一次请求中查询所有的信息,这里查询整个帖子我们可以执行下面的语句:
> db.posts.findOne( { _id : ObjectId("4e77bb3b8a3e000000004f7a") } );
如果要查询所有的帖子,我们可以按照下面的写法:
> db.posts.find( { author : "alex" } )
如果上面的是一种常见的查询,我们将创建一个索引在author字段上:
> db.posts.ensureIndex( { author : 1 } )
文档可以是非常大的,根据标题来查询帖子,可以参照下面的语句:
> db.posts.find( { author : "alex" }, { title : 1 } )
或者,我们可能会按照帖子的标签来查询相关贴子:
> // make and index of all tags so that the query is fast: > db.posts.ensureIndex( { tags : 1 } ) > db.posts.find( { tags : "business" } )
假如我们想找到所有 meghan 写的帖子呢?
> db.posts.find( { comments.who : "meghan" } )
加上索引使查询速度更快:
> db.posts.ensureIndex( { "comments.who" : 1 } )
我们跟踪上面的投票人,不允许一个人对一个帖子投两次票。假设 calvin 想给上面示例中的帖子投票,以下更新操作将记录加尔文的投票。因为$ nin子表达式来检测作用,所以如果卡尔文已投票,更新将不会有效。
> db.posts.update( { _id : ObjectId("4e77bb3b8a3e000000004f7a"), voters : { $nin : "calvin" } }, { votes : { $inc : 1 }, voters : { $push : "calvin" } );
注意:上面的操作是,如果多个用户同时投票,投票依然不会丢失。
假设我们想在系统中显示最新的文章的标题以及作者完整的用户名,在这种情况下,我们必须使用客户端连接:
> var post = db.posts.find().sort( { when : -1 } ).limit(1); > var user = db.users.find( { _id : post.author } ); > print( post.title + " " + user.name.first + " " + user.name.last );
最后一个问题,我们可能会问关于我们的例子我们是如何分片的。如果用户的集合是比较小的,那我们就不需要切分它,如果文章是巨大的,我们切分它。我们需要选择一个分片键,分片键的选择应该以常见的查询为基础。
1.是否按照 _id 切分是可选的。
2.如果发现[最近的帖子]是一个非常频繁的查询,然后我们会在 when 这个字段上做切分。()
Summary of Best Practices:最佳实践的摘要
1.顶级对象的特点是它们有自己的集合(比如帖子对象就是顶级对象,标签就是次级对象)。
2.排列项的详情对象的特点是嵌入的(比如帖子的标签属性对象是嵌入在帖子对象里面的)。
3.如果对象之间符合包含关系的模型,那么通常应该被嵌入。
4.多对多关系通常是做链接(例如帖子和投票者)。
5.只有几个对象集合可能安全地作为单独的集合存在,因为整个集合可以快速被缓存在应用程序服务器中。
6.相比于顶级对象,嵌入的对象比较难以获取。
7.当需要一个用到MongoDB的工具的操作的时候,得到嵌入对象的系统基本的视图是非常困难的(不解)。
8.如果嵌入的对象的数据量是巨大的(N兆字节),你可能会达到一个对象大小的极限。参见。
9.如果性能是一个问题,那么选择嵌入。
More Details:更多细节 |
选择索引
Schema设计的第二个方面是索引的选择。作为一般规则,像在关系数据库中需要索引一样,MongoDB也同样需要索引。
1._id字段是自动索引。
2.查询频率比较高的字段上也需要索引。
3.排序字段通常应该被索引。
MongoDB提供了比较有用的信息,关于哪里缺少索引需要添加。
注意,添加一个索引放缓写到一个集合,但不读。为集合使用大量高读写比率的索引(假设你不介意存储超额)。对于写多于读的集合,索引是非常重要的键必须对每一次插入添加索引(不解)。
有多少集合?
因为Mongo集合是多态的,所以你只要有一个集合对象,那么你可以把所有东西都放进去!这种方法被一些对象数据库所吸引。在MongoDB中这是不推荐,有几个原因,主要在于性能。 在单个集合里的数据数据大部分是连续的磁盘上,因此,一个集合的“表扫描”是可能的,有效的,就像在关系数据库中,独立的集合是非常重要对于高吞吐量的批处理。
See Also:参见 |
书籍 1. - O'Reilly Ebook 2.
博客文章 Blog post on dynamic schemas
相关文档页面
视频 ——MongoSF演示(2011年5月) ——MongoSF演示(2011年5月) ——MongoSV演示(2010年12月)
Tip:本人的英语不好,大部分地方算是直译吧,所以比较差,只是想抛砖引玉,感兴趣的童鞋可以看看原文。本人读完了解的也只是皮毛,仅供参考,更详细内容请参考官网页面:
本页翻译完毕——于:2012年11月8日