6 Rules of Thumb for MongoDB Schema Design: Part 2javascript
做者 William Zola, Lead Technical Support Engineer at MongoDBjava
(请结合上一篇 MongoDB Shema 设计的6条经验法则1一块儿阅读。)mongodb
关于 在 MongoDB 中为 1对多
关系建模的旅程中的第二站,上一次我谈过了三点基本的 Schema 设计:内嵌,子引用和父引用。还谈到了在选择这些设计时要考虑两个因素:数组
1对多
中多端
的实体须要独立存在吗?网络
这个关系的基数是什么样的:1对少
,1对不少
,仍是1对很是多?less
有了这些基础技术知识的保障,我能够继续讨论更多复杂的 Schema 设计, 会涉及 双向引用和反范式化。ide
若是你想你的 Scheme 设计变得更高级一点,你能够结合两个技术而且在你的Schema同时使用着两种引用,从1端
到多端
的引用和从多端
到1端
的引用。post
好比,让咱们回到任务追踪系统。系统中 People
集合保存 Person
文档, Task
集合保存 Task
文档,以及一个 一对多
的从Person ->Task
的关系。这个应用须要追踪全部的一个Person
全部的 Task
。优化
Task
文档中有一个引用数组, Person
文档可能看起来像下面这样:ui
db.person.findOne()
{
_id: ObjectID("AAF1"),
name: "Kate Monster",
tasks [ // array of references to Task documents
ObjectID("ADF9"),
ObjectID("AE02"),
ObjectID("AE73")
// etc
]
}
复制代码
另外一方面,这个应用的其余部分要显示一个 Task 列表 (例如, 显示在多人项目中的全部任务)而且这个列表还须要快速的查找到一个 Person
应当负责的全部任务。你能够经过在Task
文档中添加一个附加的引用来对其进行优化。
db.tasks.findOne()
{
_id: ObjectID("ADF9"),
description: "Write lesson plan",
due_date: ISODate("2014-04-01"),
owner: ObjectID("AAF1") // Reference to Person document
}
复制代码
这个设计具备 一对不少 Schema 全部的有点和缺点,但还有一些附加优势。在 Task
文档中加入一个额外的 owner
引用意味着它能够很容易地作到快速地找到任务的全部者。但同时也意味着,若是你从新分配任务给另一我的,你须要执行两条更新。 特别是,你必须同时更新从 Person 到 Task 的应用,以及从 Task 到 Person 的引用。(对于正在阅读这篇文章的专家来讲——没错, 使用这个Schema设计 意味着 再也不可能从新将任务分配给一个新的 Person
。 这对咱们的任务追踪系统是能够的: 你须要考虑这是否对你的特定方案有效。)
对于零件的例子, 你能够反范式化零件的名字到 parts[] 数组。做为参考,这是 Product 文档反范式化版本。
> 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
]
}
复制代码
反范式化意味着在显示产品全部的零件名称时将没必要执行应用级别的联接,但若是你须要关于零件的其余信息,则必须得执行该联接。
> db.products.findOne()
{
name : 'left-handed smoke shifter',
manufacturer : 'Acme Corp',
catalog_number: 1234,
parts : [
{ id : ObjectID('AAAA'), name : '#4 grommet' }, // Part name is denormalized
{ id: ObjectID('F17C'), name : 'fan blade assembly' },
{ id: ObjectID('D2AA'), name : 'power switch' },
// etc
]
}
复制代码
虽然零件名称的获取变得容易了,但这也在应用级的联接中增长了一点客户端的工做。
// Fetch the product document
> product = db.products.findOne({catalog_number: 1234});
// Create an array of ObjectID()s containing *just* the part numbers
> part_ids = product.parts.map( function(doc) { return doc.id } );
// Fetch all the Parts that are linked to this Product
> product_parts = db.parts.find({_id: { $in : part_ids } } ).toArray() ;
复制代码
反范式化节约了对反范式化数据查找的成本,倒是以一个更高成本的更新为代价。 若是你在已经将 Part
的Name
反范式化到了Product
文档中, 那么在更新 Part
的名称时,你必须同时更新 Products
集合中每一条出现这个零件的数据。
反范式化只当在读取与更新比例很高时才有意义。若是你须要频繁的读取这些反范式化的数据,却不多对它做更新操做,那么为了获得更有效的查询,一般须要以更新变得缓慢而复杂做为代价。当更新相对于查询更为频繁时,反范式化所节省下的成本则变少了。
例如,假设零件的名称常常不常常改动,但现有的零件的数据常常变更。这意味着虽然将 Part
的 Name
范式化到 Product
文档中有意义,现有零件的数据的反范式化则没有意义。
同时注意,当你范式化一个字段时,你将没法对这个字段进行独立原子更新。就像上面的双向引用的示例,若是你先在 Part
文档中更新零件的名字,而后在 Product
文档中更新零件的名字,将会出现一个不到一秒的时间间隔,这会形成Product
文档中将不会映射到 Part
文档中心得更新过的值。
1对不少
的反范式化你也是对自动实现从 1端 到 多端的反范式化:
> db.parts.findOne()
{
_id : ObjectID('AAAA'),
partno : '123-aff-456',
name : '#4 grommet',
product_name : 'left-handed smoke shifter', // Denormalized from the ‘Product’ document
product_catalog_number: 1234, // Ditto
qty: 94,
cost: 0.94,
price: 3.99
}
复制代码
然而,若是你已经完成了从 Product
名称 到 Part
文档的范式化,那么当你更新 Product
名称时,你也必须同时更新 Part
集合中每个 Product
出现的地方。这多是一个成本更高的更新,由于你同时更新了的多个 Parts
。 所以,在进行这样的反范式化时,读与写比例的考虑就显是尤其重要了。
1对很是多
的示例也能够进行反范式化,经过如下两种方式:你能够将 1端
的信息(来自 hots
文档)放入 很是多
的那一端, 或者你也能够放入一些很是多端
的总结性的信息到1端
。
下面是反范式化到很是多端
的示例。我将把 host的IP地址 放入到单个的日志消息中:
> db.logmsg.findOne()
{
time : ISODate("2014-03-28T09:42:41.382Z"),
message : 'cpu is on fire!',
ipaddr : '127.66.66.66',
host: ObjectID('AAAB')
}
复制代码
对于查询特定IP的最新消息会变得更加容易。如今只须要一次查询:
> last_5k_msg = db.logmsg.find({ipaddr : '127.66.66.66'}).sort({time : -1}).limit(5000).toArray()
复制代码
实际上,若是你只想在 1端
存放必定数量的信息,你能够将它所有都反范式化到 很是多
那一段,彻底没必要用到1端
。
> db.logmsg.findOne()
{
time : ISODate("2014-03-28T09:42:41.382Z"),
message : 'cpu is on fire!',
ipaddr : '127.66.66.66',
hostname : 'goofy.example.com',
}
复制代码
另外一方面,你有能够反范式化到 1端
。假如你想在 host 文档中保存一个 host
最近1000条信息。你可使用MongoDB 2.4中 介绍的 $each
和 $slice
方法来保存那只包含了最近1000 条且已排好序的信息。
日志信息被保存到了 logmsg
集合中,同时也保存到了 host
文档中的反范式化列表里。 这样,即便信息超出了 hosts
。logmsgs
数组,也不会丢失了
// Get log message from monitoring system
logmsg = get_log_msg();
log_message_here = logmsg.msg;
log_ip = logmsg.ipaddr;
// Get current timestamp
now = new Date()
// Find the _id for the host I’m updating
host_doc = db.hosts.findOne({ipaddr : log_ip },{_id:1}); // Don’t return the whole document
host_id = host_doc._id;
// Insert the log message, the parent reference, and the denormalized data into the ‘many’ side
db.logmsg.save({time : now, message : log_message_here, ipaddr : log_ip, host : host_id ) });
// Push the denormalized log message onto the ‘one’ side
db.hosts.update( {_id: host_id },
{$push : {logmsgs : { $each: [ { time : now, message : log_message_here } ],
$sort: { time : 1 }, // Only keep the latest ones
$slice: -1000 } // Only keep the latest 1000
}} );
复制代码
须要注意,投影规划可防止MongoDB在网络中传输整个 hosts 文档。经过告知 MongoDB只须要返回 _id
字段,网络开销能够减小到仅用于村塾该字段的那个几个字节(还有一些传输协议的开销)。
就像 一对多 案例中同样,你须要考虑读取与更新的比例。只有当日志消息相对应用程序须要在全部消息查找单个主机的次数不多时,日志消息到Host文档的反范式化才又意义。若是你要查找数据的频率低于更新的频率,那这种特殊的反范式化则不是一个好的办法。
在这篇文章中,我谈到了在基本的内嵌,子引用和父引用的其余选择。
若是双向引用能够优化你的 Schema,而且你能够接受不能进行原子更新这样的代价,那么就可使用双向引用。
引用时,从 1端 到 N 端
,或者 N端 到 1端
的反范式化都是能够的,
在以为是否须要反范式化是,要考虑下面的因素:
你将没法对反范式化的数据进行原子更新。
在读较写的比例高时, 反范式化才有意义。
下一次,我给大家一些关于这些选项的选择上的指导。