【译】MongoDB Shema 设计的6条经验法则 1

6 Rules of Thumb for MongoDB Schema Design: Part 1 javascript

做者 William Zola, Lead Technical Support Engineer at MongoDBhtml

“我有不少使用SQL的经验, 但在对于 MongoDB 还只是一个初学者。我应该如何为 一对多 的关系建模呢?” 这是我工做时从 MongoDB 用户收到很常见问题。java

对于这个问题我没办法给出一个简短的答案,由于方法有不少,不止某一种。MongoDB 有一个丰富而细微的语汇去表达在SQL中被扁平化为术语 “1 to N ” 的内容。mongodb

这个话题有不少能够讲的,我将其分红三个部分。数据库

第一个部分,我会谈三个基础的方法去创建 1对多 关系模型。数组

第二个话题,会涉及复杂的 Shema 设计,包括反范式化和双向引用。数据库设计

最后一部分,我会回顾各类各样的选择,并提供一些在考虑构建一个 1对多 关系模型时的建议。ide

许多初学者认为 MongoDB 中构建 1对多关系模型的惟一方法是 在父文档中嵌入一个子文档数组,但这个是不对的。你能够嵌入一个文档,但不意味着你应该嵌入一个文档。post

在设计 MongoDB Shema 时, 你须要从一个你在使用 SQL 时历来没想过的问题开始:这个关系的 基数 (cardinality,指 N 端的数据规模 ) 是怎么样的?简言之:你须要更加细微地表述你的 1对多 关系:它是 1对少1对不少,仍是 1 对很是多?根据它是哪一种,你能够去选择不一样的方式对关系建模。性能

基础知识:1对少 建模

一我的的地址能够做为 1对少 的示例。这个是一个关于内嵌子文档的好案例 ——在 Person 对象中嵌入一组 address 文档:

> db.person.findOne()
{
  name: 'Kate Monster',
  ssn: '123-456-7890',
  addresses : [
     { street: '123 Sesame St', city: 'Anytown', cc: 'USA' },
     { street: '123 Avenue Q', city: 'New York', cc: 'USA' }
  ]
}

复制代码

这种设计具备内嵌子文档的全部优缺点。主要的优势是 没必要执行一个单独查询来获取内嵌的资料,主要的缺点则是内嵌的资料没办法做为一个单独的实体进行访问。

例如,你在为一个任务追踪系统建模,每个 Person 都会有不少任务分配到他们。在Person 文档中的内嵌任务会让 如显示因为明天到期的任务 这样的查询变得比其实际须要的更加困难。对于这种案例,我会在下一个部分提供一个更合适的设计。

基础知识:1对不少 建模

替换零部件订购系统中产品的零部件能够做为 1对多 的示例。每个产品可能都有几百个替换李建,但毫不会超过几千个左右(全部这些不一样尺寸的螺栓,垫圈和垫圈加起来)。这是一个很好的参考案例,你能够将零件的 ObjectID 内嵌到产品的问题中。(这个示例中,我使用2字节的 ObjectID,由于它们更易于阅读:实际代码将使用12字节的 ObjectID

每个零部件 _( parts )_会有它们的本身的文档:

> db.parts.findOne()
{
    _id : ObjectID('AAAA'),
    partno : '123-aff-456',
    name : '#4 grommet',
    qty: 94,
    cost: 0.94,
    price: 3.99
}
复制代码

每个产品 _( products )_也会有它们本身的文档,其文档中还会因此包含组成产品的 partsObjectID 数组:

> db.products.findOne()
{
    name : 'left-handed smoke shifter',
    manufacturer : 'Acme Corp',
    catalog_number: 1234,
    parts : [     // array of references to Part documents
        ObjectID('AAAA'),    // reference to the #4 grommet above
        ObjectID('F17C'),    // reference to a different Part
        ObjectID('D2AA'),
        // etc
    ]
}
复制代码

而后,您再使用应用程序级联接来检索特定产品的零件:

// Fetch the Product document identified by this catalog number
> product = db.products.findOne({catalog_number: 1234});
   // Fetch all the Parts that are linked to this Product
> product_parts = db.parts.find({_id: { $in : product.parts } } ).toArray() ;
复制代码

为了操做更加高效,你可能还须要为products.catalog_number 创建索引。注意parts._id必定有索引的,所以查询 parts 会很快。

这种风格的引用能与内嵌文档的优缺点起到互补的左右。每个零件都是一个单独的文档,所以很容易对他们作独立的查询和更新。使用这个 Schema 也须要一点妥协,就是在获取查询零部件详细信息时须要在再作一次查询。(但先保留这个问题,直到咱们进入 part 2 - 反范式化)。

做为一个额外的好处,这种 Schema 容许你在多个产品中使用不一样的零件,因此你的 1对多 Schema 就变成了多对多的 Schema 不须要任何关联表了。

基础知识:1对很是多 建模

从不一样机器上收集日志信息的事件日志系统能够做为 1对不少 的示例。任何给定 host 能够生产超过16Mb 的文档大小,即便你的的数组中村咋都是 ObjectID 。这是一个 “父引用”经典案例 — 你可有一个host 文档,而且在每个日志信息文档中存储这个 Host 的 ObjectID 。

> db.hosts.findOne()
{
    _id : ObjectID('AAAB'),
    name : 'goofy.example.com',
    ipaddr : '127.66.66.66'
}

>db.logmsg.findOne()
{
    time : ISODate("2014-03-28T09:42:41.382Z"),
    message : 'cpu is on fire!',
    host: ObjectID('AAAB')       // Reference to the Host document
}
复制代码

你能够用一个有一点点不一样的应用级别的联合查询获得一个host最近5000条Messages:

// find the parent ‘host’ document
> host = db.hosts.findOne({ipaddr : '127.66.66.66'});  // assumes unique index
   // find the most recent 5000 log message documents linked to that host
> last_5k_msg = db.logmsg.find({host: host._id}).sort({time : -1}).limit(5000).toArray()
复制代码

回顾

如此可见,即便在这个基础的级别,设计 MongoDB Schema 时 比 设计一个对比的 关系性的 Schema要思考的更多。有如下两个的因素须要考虑:

  1. 1对多 中"多"端的实体须要独立存在吗?

  2. 这个关系的基数是什么样的:1对少,1对不少,仍是1对很是多?

基于这些因素,你能够选择三个基本的 1对多 Schema 设计:

  • 若是基数是 1对少而且不须要从父对象的上下文以外访问内嵌的对象,则直接内嵌N端;

  • 若是基数为一对多 或者 N 端对象因故须要独立存在,则内嵌 N 端对象的引用数组。

  • 若是基数为一对不少,则在 N 端对象中引用 1端对象。

下一次,咱们将了解如何使用双向关系和非规范化来加强这些基础的 Schema 性能。

写在后面:

最开始是在这篇文章 MongoDB数据库设计中6条重要的经验法则 里看到的,可是由于排版不是很容易阅读,所以我直接阅读了原文,并本身试着重新翻译排版, 部分术语的理解也参考了 MongoDB数据库设计中6条重要的经验法则

文章虽然已经比较久了,但对我才开始使用的 MongoDB 的初学者来说,里面的知识点仍是很是有价值的。后面还有两篇,会接着看完并翻译发表。但愿对看到的人也有所帮助。

相关文章
相关标签/搜索