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对少 的示例。这个是一个关于内嵌子文档的好案例 ——在 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对多
的示例。每个产品可能都有几百个替换李建,但毫不会超过几千个左右(全部这些不一样尺寸的螺栓,垫圈和垫圈加起来)。这是一个很好的参考案例,你能够将零件的 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 )_也会有它们本身的文档,其文档中还会因此包含组成产品的 parts
的 ObjectID
数组:
> 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对不少
的示例。任何给定 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对少,1对不少,仍是1对很是多?
基于这些因素,你能够选择三个基本的 1对多 Schema 设计:
若是基数是 1对少而且不须要从父对象的上下文以外访问内嵌的对象,则直接内嵌N端;
若是基数为一对多 或者 N 端对象因故须要独立存在,则内嵌 N 端对象的引用数组。
若是基数为一对不少,则在 N 端对象中引用 1端对象。
下一次,咱们将了解如何使用双向关系和非规范化来加强这些基础的 Schema 性能。
最开始是在这篇文章 MongoDB数据库设计中6条重要的经验法则 里看到的,可是由于排版不是很容易阅读,所以我直接阅读了原文,并本身试着重新翻译排版, 部分术语的理解也参考了 MongoDB数据库设计中6条重要的经验法则 。
文章虽然已经比较久了,但对我才开始使用的 MongoDB 的初学者来说,里面的知识点仍是很是有价值的。后面还有两篇,会接着看完并翻译发表。但愿对看到的人也有所帮助。