MongoDB循序渐进之[特性]——面向文档存储

 

Document-Oriented Storage
JSON-style documents with dynamic schemas offer simplicity and power.


面向文档存储

JSON风格的文件与动态模式使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的map/reduce工具的操作的时候,得到嵌入对象的系统基本的视图是非常困难的(不解)。

8.如果嵌入的对象的数据量是巨大的(N兆字节),你可能会达到一个对象大小的极限。参见网格文件系统

9.如果性能是一个问题,那么选择嵌入。

 

  More Details:更多细节

选择索引

   Schema设计的第二个方面是索引的选择。作为一般规则,像在关系数据库中需要索引一样,MongoDB也同样需要索引。

1._id字段是自动索引。

2.查询频率比较高的字段上也需要索引。

3.排序字段通常应该被索引。

   MongoDB分析工具提供了比较有用的信息,关于哪里缺少索引需要添加。

注意,添加一个索引放缓写到一个集合,但不读。为集合使用大量高读写比率的索引(假设你不介意存储超额)。对于写多于读的集合,索引是非常重要的键必须对每一次插入添加索引(不解)。

有多少集合?

   因为Mongo集合是多态的,所以你只要有一个集合对象,那么你可以把所有东西都放进去!这种方法被一些对象数据库所吸引。在MongoDB中这是不推荐,有几个原因,主要在于性能。 在单个集合里的数据数据大部分是连续的磁盘上,因此,一个集合的“表扫描”是可能的,有效的,就像在关系数据库中,独立的集合是非常重要对于高吞吐量的批处理。

 

  See Also:参见

书籍
1.
MongoDB文档设计 - O'Reilly Ebook
2.
更多书籍

博客文章
http://blog.fiesta.cc/post/11319522700/walkthrough-mongodb-data-modeling
Blog post on dynamic schemas

相关文档页面
MongoDB中的树
MongoDB数据建模和Rails
高级的查询

视频
模式设计基础——MongoSF演示(2011年5月)
模式设计规模——MongoSF演示(2011年5月)
模式设计:文档型数据——MongoSV演示(2010年12月)

Tip:本人的英语不好,大部分地方算是直译吧,所以比较差,只是想抛砖引玉,感兴趣的童鞋可以看看原文。本人读完了解的也只是皮毛,仅供参考,更详细内容请参考官网页面:http://www.mongodb.org/display/DOCS/Schema+Design

                                 本页翻译完毕——于:2012年11月8日

posted @ 2012-12-04 14:12  DolphinBoy  阅读(735)  评论(4编辑  收藏  举报