嵌入还是引用
在构建新应用时,第一件事情就是设计数据库模型。在关系型数据库中,设计数据模型是在标准化过程中进行规范化,重点关注从一组表格中移除冗余。MongoDB使用结构化的文档来存储数据,而非存储在关系型数据库中固定表格内。典型的关系型表格需每一个行列交汇处有一个标量的值。MongoDB的BSON文档通过支持值数组来支持更为复杂的结构。
关系型数据模型和标准化
在关系型数据库中,数据建模是使用一系列表来建立数据模型,表 由行列组成,行和列共同定义了数据的模式。关系型数据库理论被定义为若干将应用数据放入表的方法,称为范式。
什么是范式
典型模式标准化是从将应用数据放入第一范式(1NF)开始的,第一范式可看做所有数据都是表格化的,每个行列交汇处都有一个确切的值的一种模式(必要条件)。
标准化带来的好处是实现了没有任何冗余的便捷更新,可简单地修改任意一个行列相交处的值来更新数据,但是读取数据时问题出现了。
旋转磁盘有一个特性是它耗费很长时间来查找磁盘上特定位置(寻址),当查找到位置后就开始从磁盘上连续读取数据。使用join读取一行时会耗费99%的时间进行寻道,对于访问磁盘而言,随机寻道是它的敌人。而典型的JOIN操作通常需要采用随机寻道。
现代数据库系统已经修改了结构来缓解随机寻道问题,主要是通过捕获内存中频繁使用的对象(特别是索引)。尽管使用这样的优化手段,对于关系型数据库来说,连接数据表仍旧是一个十分昂贵的操作。除此之外,若日后将数据库扩展到多个服务器上,将会引起分布式连接的问题,这是十分复杂的操作且执行非常缓慢。
采用非标准来提供性能
关系型数据库有一个不可告人的秘密,这个秘密是通过数据建模操作来生成优美的N范式数据模型后,通常有必要对模型进行非标准来减少频繁查询所需的JOIN操作。
谁需要标准化呢?
MongoDB的数据不必总是列成表格,基本上否决了传统数据库从1NF开始进行标准化的操作。MongoDB中数据存储在文档中,意味着在RDB中的1NF需要每个行列交汇处都必须有且仅有一个值,而MongoDB允许根据需要在行列交汇处存储一个数组。
MongoDB可原生地编码多值数据,用户可获取性能收益,因为非标准化使得用户无需困难地更新冗余数据。不幸的是这同时也使得模式的设计变得更为复杂。这里不再有一个标准化数据库设计所遵循的“花园路径”,在使用MongoDB时遇到一般模式问题的需要回答的是“它依赖什么”。
MongoDB文档格式
MongoDB中的文档是使用JSON(JavaScript Object Notation)格式建模的,但实际上是存储在BSON(Binary JSON)中。简要地说这意味着MongoDB文档是一个键值对的字典。
// 手机通讯录
{
_id:3,
name:'jenny',
zipcode:'01209',
mobiles:['555-333-3456', '555-334-3411']
}
// 标准化操作
{_id:3, name:'jenny', zipcode:'01209'}
{contact_id:3, mobile:'555-333-3456'}
{contact_id:3, mobile:'555-334-3411'}
问题是什么情形下该使用引用,什么情况下又该使用嵌入呢?
局部性嵌入
数据局部性是用户希望嵌入一对多关系的原因之一。旋转磁盘在持续数据传输方面有较好的性能,而在随机寻道方面性能很差。MongoDB在磁盘上持续存储文档,将所需的数据存放在同一个文档中,意味着仅需执行一次寻道操作即可获取所需数据。
原子性和独立性嵌入
在使用嵌入时我们关心的是写入数据时保持原子性和独立性,当更新数据库中数据时,用户希望确保更新操作要么是完全成功,要么是失败,而不是出现“部分成功”的现象,其他数据库的用户不会看到一个完整的写操作。
MongoDB为什么没有事务呢?
MongoDB底层设计考虑了如何简便地扩展到分布的多台服务器上,分布式数据库设计上两个最大的问题是分布式join操作和分布式事务。这两个操作都是很难实现的,因为在某一个服务器不可访问时将会导致极大的性能下降。通过在这个问题上对赌,不支持join和多文档事务,MongoDB可实现自动的分片解决方案,能获得比坚持实现join和事务更好的扩展性和性能。
一个有趣的事情是很多关系型数据库放松了事务必须和其他事务完全隔离的需求,引入了多个隔离级别。因此,若将升级操作构建为只针对单个文档的更新,即可获得序列化的隔离级别的优势,不会有关系型是数据库所带来的性能上的损失。
为了灵活性采用引用
嵌入能够提供最好的性能和数据完整性保证,然后某些情形下更标准化的模型在MongoDB中能获得更好的性能。考虑到将用户数据模型标准化到多个容器,将会提升执行查询的灵活性。
总体而言,若应用的查询模式是常见的并倾向于采用一种方式访问数据,那么嵌入式的途径可以很好的工作。若应用可能采用多种方式查询数据,或无法预计数据查询的模式,那么采用更标准化的方式可能更好。
为了潜在的高引数关系使用引用
使用文档引用的重要因素是在,有十分高或无法预测的引数的一对多关系时。
例如,拥有大量读者参与度的流行博客中,某博客文章可能有成百上千的评论。这种情况下,嵌入式会带来明显的损失。
- 文档越大,使用的内存就越大。
- 增长的文档最终必须拷贝到更大的空间中。
- MongoDB文档最大不得超过16MB。
在MongoDB中内存消耗是一个非常严重的问题,内存是MongoDB服务器重要的资源,MongoDB数据库将频繁访问的文档缓存在内存中。当文档越来越大时,缓存的文档越少。内存中文档越少,服务器发生读取文档页面错误的可能性就越大,最终页面错误会导致随机的磁盘I/O。
当文档大小增长时需进行拷贝,这和更新操作性能有关。当为嵌入的评论数组增加评论时,最终MongoDB将需要把文档移动到一个更多可用空间的区域。当发生这种操作时会明显的降低更新数据的性能。
MongoDB文档大小的限制意味着若你的关系中有一个潜在的无限制引数,就有可能运行超出全部空间,将会阻止在该条目上增加新的评论。尽管这是偶尔需要考虑的事情,通过在到达16MB大小限制之前,就会遇到内存压力和稳定拷贝压力。
多对多关系
使用文档引用常使用在多对多关系(M:N)中,对于多对多折中的方法是使用嵌入_id的列表,而不是嵌入完整的文档。
总结
MongoDB中的模式设计更倾向于一个艺术问题而非科学技术问题。早期需要做的一个决定是采用一对多嵌入为子文档的数组呢,还是采用关系型通过_id值来进行引用呢。
嵌入子文档的好处在于,数据局部保存在文档中,MongoDB有能力对单个文档执行原子更新,但不能对连个文档进行。相对着两个优势,嵌入式会降低灵活度,因为已经完成对文档的预联合,若有一个高引数的关系,会存在潜在的问题。