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

2,946 阅读6分钟

6 Rules of Thumb for MongoDB Schema Design: Part 1

作者 William Zola, Lead Technical Support Engineer at MongoDB

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

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

这个话题有很多可以讲的,我将其分成三个部分。

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

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

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

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

在设计 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 的初学者来讲,里面的知识点还是非常有价值的。后面还有两篇,会接着看完并翻译发表。希望对看到的人也有所帮助。