Part 1
原⽂:
By William Zola, Lead Technical Support Engineer at MongoDB
“我有丰富的sql使⽤经验,但是我是个MongoDB的初学者。我应该如何在MongoDB中针对⼀对多关系进⾏建模?”这是我被问及最多的问题之⼀。我没法简单的给出答案,因为这有很多⽅案去实现。接下来我会教导你如何针对⼀对多进⾏建模。
这个话题有很多内容需要讨论,我会⽤三个部分进⾏说明。在第⼀部分,我会讨论针对⼀对多关系建模的三种基础⽅案。在第⼆部分我将会覆盖更多⾼级内容,包括反范式化和双向引⽤。在最后⼀部分,我将会回顾各种选择,并给出做决定时需要考虑的因素。
很多初学者认为在MongoDB中针对⼀对多建模唯⼀的⽅案就是在⽗⽂档中内嵌⼀个数组⼦⽂档,但是这是不准确的。因为你可以在MongoDB内嵌⼀个⽂档不代表你就必须这么做。
当你设计⼀个MongoDB数据库结构,你需要先问⾃⼰⼀个在使⽤关系型数据库时不会考虑的问题:这个关系中集合的⼤⼩是什么样的规模?你需要意识到⼀对很少,⼀对许多,⼀对⾮常多,这些细微的区别。不同的情况下你的建模也将不同。Basics: Modeling One-to-Few⼀对很少
针对个⼈需要保存多个地址进⾏建模的场景下使⽤内嵌⽂档是很合适,可以在person⽂档中嵌⼊addresses数组⽂档:
这种设计具有内嵌⽂档设计中所有的优缺点。最主要的优点就是不需要单独执⾏⼀条语句去获取内嵌的内容。最主要的缺点是你⽆法把这些内嵌⽂档当做单独的实体去访问。
例如,如果你是在对⼀个任务跟踪系统进⾏建模,每个⽤户将会被分配若⼲个任务。内嵌这些任务到⽤户⽂档在遇到“查询昨天所有的任务”这样的问题时将会⾮常困难。我会在下⼀篇⽂章针对这个⽤例提供⼀些适当的设计。Basics: One-to-Many⼀对许多
以产品零件订货系统为例。每个商品有数百个可替换的零件,但是不会超过数千个。这个⽤例很适合使⽤间接引⽤---将零件的objectid作为数组存放在商品⽂档中(在这个例⼦中的ObjectID我使⽤更加易读的2字节,现实世界中他们可能是由12个字节组成的)。每个零件都将有他们⾃⼰的⽂档对象
每个产品的⽂档对象中parts数组中将会存放多个零件的ObjectID :
在获取特定产品中所有零件,需要⼀个应⽤层级别的join
为了能快速的执⾏查询,必须确保products.catalog_number有索引。当然由于零件中parts._id⼀定是有索引的,所以这也会很⾼效。
这种引⽤的⽅式是对内嵌优缺点的补充。每个零件是个单独的⽂档,可以很容易的独⽴去搜索和更新他们。需要⼀条单独的语句去获取零件的具体内容是使⽤这种建模⽅式需要考虑的⼀个问题(请仔细思考这个问题,在第⼆章反反范式化中,我们还会讨论这个问题)这种建模⽅式中的零件部分可以被多个产品使⽤,所以在多对多时不需要⼀张单独的连接表。Basics: One-to-Squillions⼀对⾮常多
我们⽤⼀个收集各种机器⽇志的例⼦来讨论⼀对⾮常多的问题。由于每个mongodb的⽂档有16M的⼤⼩限制,所以即使你是存储ObjectID也是不够的。我们可以使⽤很经典的处理⽅法“⽗级引⽤”---⽤⼀个⽂档存储主机,在每个⽇志⽂档中保存这个主机的ObjectID。
以下是个和第⼆中⽅案稍微不同的应⽤级别的join⽤来查找⼀台主机最近5000条的⽇志信息
所以,即使这种简单的讨论也有能察觉出mongobd的建模和关系模型建模的不同之处。你必须要注意⼀下两个因素:Will the entities on the “N” side of the One-to-N ever need to stand alone?⼀对多中的多是否需要⼀个单独的实体。
What is the cardinality of the relationship: is it one-to-few; one-to-many; or one-to-squillions?这个关系中集合的规模是⼀对很少,很多,还是⾮常多。
Based on these factors, you can pick one of the three basic One-to-N schema designs:基于以上因素来决定采取⼀下三种建模的⽅式
⼀对很少且不需要单独访问内嵌内容的情况下可以使⽤内嵌多的⼀⽅。
⼀对多且多的⼀端内容因为各种理由需要单独存在的情况下可以通过数组的⽅式引⽤多的⼀⽅的。⼀对⾮常多的情况下,请将⼀的那端引⽤嵌⼊进多的⼀端对象中。
Part 2
原⽂:
By William Zola, Lead Technical Support Engineer at MongoDB
在上⼀篇⽂章中我介绍了三种基本的设计⽅案:内嵌,⼦引⽤,⽗引⽤,同时说明了在选择⽅案时需要考虑的两个关键因素。⼀对多中的多是否需要⼀个单独的实体。
这个关系中集合的规模是⼀对很少,很多,还是⾮常多。
在掌握了以上基础技术后,我将会介绍更为⾼级的主题:双向关联和反范式化。双向关联
如果你想让你的设计更酷,你可以让引⽤的“one”端和“many”端同时保存对⽅的引⽤。
以上⼀篇⽂章讨论过的任务跟踪系统为例。有person和task两个集合,one-to-n的关系是从person端到task端。在需要获取person所有的task这个场景下需要在person这个对象中保存有task的id数组,如下⾯代码所⽰。
在某些场景中这个应⽤需要显⽰任务的列表(例如显⽰⼀个多⼈协作项⽬中所有的任务),为了能够快速的获取某个⽤户负责的项⽬可以在task对象中嵌⼊附加的person引⽤关系。
这个⽅案具有所有的⼀对多⽅案的优缺点,但是通过添加附加的引⽤关系。在task⽂档对象中添加额外的“owner”引⽤可以很快的找到某个task的所有者,但是如果想将⼀个task分配给其他person就需要更新引⽤中的person和task这两个对象(熟悉关系数据库的童鞋会发现这样就没法保证操作的原⼦性。当然,这对任务跟踪系统来说并没有什么问题,但是你必须考虑你的⽤例是否能够容忍)在⼀对多关系中应⽤反范式
在你的设计中加⼊反范式,可以使你避免应⽤层级别的join读取,当然,代价是这也会让你在更新是需要操作更多数据。下⾯我会举个例⼦来进⾏说明
反范式Many -< One
以产品和零件为例,你可以在parts数组中冗余存储零件的名字。以下是没有加⼊反范式设计的结构。
反范式化意味着你不需要执⾏⼀个应⽤层级别的join去显⽰⼀个产品所有的零件名字,当然如果你同时还需要其他零件信息那这个应⽤层的join是避免不了的。
在使得获取零件名字简单的同时,执⾏⼀个应⽤层级别的join会和之前的代码有些区别,具体如下:
反范式化在节省你读的代价的同时会带来更新的代价:如果你将零件的名字冗余到产品的⽂档对象中,那么你想更改某个零件的名字你就必须同时更新所有包含这个零件的产品对象。
在⼀个读⽐写频率⾼的多的系统⾥,反范式是有使⽤的意义的。如果你很经常的需要⾼效的读取冗余的数据,但是⼏乎不去变更他d话,那么付出更新上的代价还是值得的。更新的频率越⾼,这种设计⽅案的带来的好处越少。
例如:假设零件的名字变化的频率很低,但是零件的库存变化很频繁,那么你可以冗余零件的名字到产品对象中,但是别冗余零件的库存。需要注意的是,⼀旦你冗余了⼀个字段,那么对于这个字段的更新将不在是原⼦的。和上⾯双向引⽤的例⼦⼀样,如果你在零件对象中更新了零件的名字,那么更新产品对象中保存的名字字段前将会存在短时间的不⼀致。反范式One -< Many
你也可以冗余one端的数据到many端:
如果你冗余产品的名字到零件表中,那么⼀旦更新产品的名字就必须更新所有和这个产品有关的零件,这⽐起只更新⼀个产品对象来说代价明显更⼤。这种情况下,更应该慎重的考虑读写频率。在⼀对很多的关系中应⽤反范式
在⽇志系统这个⼀对许多的例⼦中也可以应⽤反范式化的技术。你可以将one端(主机对象)冗余到⽇志对象中,或者反之。下⾯的例⼦将主机中的IP地址冗余到⽇志对象中。
如果想获取最近某个ip地址的⽇志信息就变的很简单,只需要⼀条语句⽽不是之前的两条就能完成。
事实上,如果one端只有少量的信息存储,你甚⾄可以全部冗余存储到多端上,合并两个对象。
另⼀⽅⾯,也可以冗余数据到one端。⽐如说你想在主机⽂档中保存最近的1000条⽇志,可以使⽤mongodb 2.4中新加⼊的$eache/$slice功能来保证list有序⽽且只保存1000条。
⽇志对象保存在logmsg集合中,同时冗余到hosts对象中。这样即使hosts对象中超过1000条的数据也不会导致⽇志对象丢失。
通过在查询中使⽤投影参数 (类似{_id:1})的⽅式在不需要使⽤logmsgs数组的情况下避免获取整个mongodb对象,1000个⽇志信息带来的⽹络开销是很⼤的。
在⼀对多的情况下,需要慎重的考虑读和更新的频率。冗余⽇志信息到主机⽂档对象中只有在⽇志对象⼏乎不会发⽣更新的情况下才是个好的决定。总结
在这篇⽂章⾥,我介绍了对三种基础⽅案:内嵌⽂档,⼦引⽤,⽗引⽤的补充选择。使⽤双向引⽤来优化你的数据库架构,前提是你能接受⽆法原⼦更新的代价。可以在引⽤关系中冗余数据到one端或者N端。在决定是否采⽤反范式化时需要考虑下⾯的因素:你将⽆法对冗余的数据进⾏原⼦更新。
只有读写⽐较⾼的情况下才应该采取反范式化的设计。
Part 3
原⽂:By William Zola, Lead Technical Support Engineer at MongoDB
这篇⽂章是系列的最后⼀篇。在第⼀篇⽂章⾥,我介绍了三种针对“⼀对多 ”关系建模的基础⽅案。在第⼆篇⽂章中,我介绍了对基础⽅案的扩展:双向关联和反范式化。
反范式可以让你避免⼀些应⽤层级别的join,但是这也会让更新变的更复杂,开销更⼤。不过冗余那些读取频率远远⼤于更新频率的字段还是值得的。
如果你还没有读过前两篇⽂章,欢迎⼀览。让我们回顾下这些⽅案
你可以采取内嵌,或者建⽴one端或者N端的引⽤,也可以三者兼⽽有之。你可以在one端或者N端冗余多个字段下⾯这些是你需要谨记的:
1、优先考虑内嵌,除⾮有什么迫不得已的原因。
2、需要单独访问⼀个对象,那这个对象就不适合被内嵌到其他对象中。
3、数组不应该⽆限制增长。如果many端有数百个⽂档对象就不要去内嵌他们可以采⽤引⽤ObjectID的⽅案;如果有数千个⽂档对象,那么就不要内嵌ObjectID的数组。该采取哪些⽅案取决于数组的⼤⼩。
4、不要害怕应⽤层级别的join:如果索引建的正确并且通过投影条件(第⼆章提及)限制返回的结果,那么应⽤层级别的join并不会⽐关系数据库中join开销⼤多少。
5、在进⾏反范式设计时请先确认读写⽐。⼀个⼏乎不更改只是读取的字段才适合冗余到其他对象中。
6、在mongodb中如何对你的数据建模,取决于你的应⽤程序如何去访问它们。数据的结构要去适应你的程序的读写场景。设计指南
当你在MongoDB中对“⼀对多”关系进⾏建模,你有很多的⽅案可供选择,所以你必须很谨慎的去考虑数据的结构。下⾯这些问题是你必须认真思考的:
关系中集合的规模有多⼤:是⼀对很少,很多,还是⾮常多?
对于⼀对多中”多“的那⼀端,是否需要单独的访问它们,还是说它们只会在⽗对象的上下⽂中被访问。被冗余的字段的读写的⽐例是多少?数据建模设计指南
在⼀对很少的情况下,你可以在⽗⽂档中内嵌数组。
在⼀对很多或者需要单独访问“N”端的数据时,你可以采⽤数组引⽤ObjectID的⽅式。如果可以加速你的访问也可以在“N”端使⽤⽗引⽤。在⼀对⾮常多的情况下,可以在“N”端使⽤⽗引⽤。
如果你打算在你的设计中引⼊冗余等反范式设计,那么你必须确保那些冗余的数据读取的频率远远⼤于更新的频率。⽽且你也不需要很强的⼀致性。因为反范式化的设计会让你在更新冗余字段时付出⼀定的代价(更慢,⾮原⼦化)
因篇幅问题不能全部显示,请点此查看更多更全内容