讲数据模型,课程介绍参见这里。
Chapter 1: Introduction to Data Modeling
需要具备的基础知识
MongoDB Concepts and Vocabulary
Database and Collection in MongoDB
Performing joins with $lookup
Relational Database Concepts and Vocabulary
Table (Wikipedia Definition)
Table (Textbook Definition)
Entity Relationship Model (Wikipedia Definition)
The Entity Relationship Data Model
Crow’s Foot Notation and ERD
Crow’s Foot Notation Definition
General Database Concepts and Definitions
Database (Wikipedia Definition)
Schema (Wikipedia Definition)
Schema Short Definition
Database Transactions (Wikipedia Definition)
Database Transactions Short Description
Throughput vs Latency
NoSQL Databases
MongoDB Compass and Atlas
Download Compass Here
More About Atlas Here
Data Modeling in MongoDB
关于MongoDB的第一个误解是其为Schemaless。MongoDB也是有Schema的,只不过它更灵活,更能适应应用的改变。有灵活的Data Model,可以通过UML或ERD(实体关系图)设计。
好的设计需要考虑:
- 使用模式
- 数据如何访问
- 哪些查询对应用重要
- 读写比例
第二个误区是将所有信息存于一个document。
第三个误区是不能做Join,$lookup
可以实现Join。
总的来说,设计好的schema是不容易的。
Data Modeling in MongoDB
数据库由collection(类似表)组成,collection由document(类似行)组成,document是JSON格式,由一系列field和value(类似列)组成,value可以是单个值或数组或嵌入document。JSON内部储存用BSON。
Document Structure in MongoDB
Supported Datatypes in MongoDB
Constraints in Data Modeling
计算机应用的约束条件包括:
- 硬件:内存,存储性能和价格
- 数据:大小,安全,数据主权(sovereignty)
- 应用:网络延迟
- 数据库:更新原子性,document最大16M
working set: 应用在典型操作中访问的数据,例如经常访问的数据和索引。
最佳实践:
- 将常用的数据置于内存
- 将索引置于内存
- 尽量选择SSD而非磁盘
- 不常用数据可置于磁盘
约束条件变了,应用设计也需相应改变。
The Data Modeling Methodology
分为3阶段:
- 描述工作负载,如数据大小,重要的读写操作
- 描述实体间的关系,分开还是嵌入
- 选择模式(Pattern)
此图缺少第3阶段,正是本课程要讲的。
Model for Simplicity or Performance
需要在简单性和性能间取得平衡。
Identifying the Workload
这一节以一个IoT的场景进行了分析,分析过程和结构参见M320-workload-IOT.xlsx。
有几点小的收获:
- 每一个读写操作可以选择不同的Write Concern和Read Concern
- hidden secondary可专门用作报表分析
Chapter 2: Relationships
Introduction to Relationships
NoSQL数据库也是relational的,其也有实体和关系。
在MongoDB中,需要考虑是嵌入还是关联。而在关系型数据库中,这些都是通过Join实现的。
Relationship Types and Cardinality
常见的关系包括1-1,1-N和N-N。例如客户ID和客户名称是1对1关系,客户和invoice是1对多关系,invoice和产品是多对多关系。
1-N关系,N也需考虑基数,例如一个人的子女与一个名人的粉丝基数差异就很大。用[min, likely, max]表示更加准确。
1-Zillion(无数)在大数据场景中常见。
1-to-Many Relationship
一个人拥有的信用卡和写的博客都是1-N关系。
1-N关系的实现,如果采用嵌入(embeded),通常嵌入到最常查询的一边;如果采用参考(reference),一般放在N端。嵌入比参考简单,如果关联信息不多,也可以考虑嵌入,这样一个查询就可以得到全部信息。
如果最常用的查询并不总是需要关联的信息,可以考虑参考。
例如:
- 嵌入到1端:文档和审阅者,由于审阅者不多,所以嵌入到文档端。
- 嵌入到N端:订单与配送地址,将配送地址嵌入到订单,因为N端查询更频繁。关系型相比,地址会有冗余,当然,冗余也是有好处的。
- 在1端参考(reference):邮编与店铺,在邮编文档中包括所有的店铺ID。
- 在N端参考:同上例,在店铺中包括邮编,似乎更合理。
Many-to-Many Relationship
例如store和item,就是N-N关系,很容易被误认为是1-N关系。
象电影和演员也是N-N关系,N-N关系需要从两端同时看,实际就是两个1-N关系。
people和phone_number也是N-N关系,因为多个人可能共享一个家庭电话。但这种关系可以转换为1-N关系,通过将家庭电话复制到每一个人的电话列表中,当家庭迁移时,就需要更新所有人的家庭电话。像这种情形,一些冗余可能是更合适的。
N-N关系可以通过embeded或reference实现。
例如,将较少查询的N端嵌入到另一N端(或者说嵌入到查询较多的一端),较少查询的N端仍然保留,虽然有数据冗余,但保证了如果嵌入的数据删除,源数据(较少查询的N端)仍存在。而且嵌入的数据也未必是全部。
嵌入的信息最好是不常变化的。
又例如,在主端加入reference,通常是一个array。当然也可以在另一端加入reference,主要看你查询什么。
reference比embeded好的地方在于可以避免重复。
One-to-One Relationship
1-1关系可通过嵌入实现,如同一级的field,或子文档中的field。好处是简单,易于理解。
通过reference实现,实际是将一个id对应的信息一劈两半,两边通过此id连接。可能是为了优化访问。
One-to-Zillions Relationship
是1-N关系的一种,只不过N特别大。
实现只有一种方式,即在Zillion那一端使用reference。
Crow’s Foot Notation Definition
Crow’s Foot Notation and ERD
Chapter 3: Patterns (Part 1)
pattern不是整体的解决方案,而是解决方案的一部分,类似于武术中的招式。
推荐了Gang of Four的Design Patterns: Elements of Reusable Object-Oriented Software一书。
Guide to Homework Validation
介绍了课程使用的作业检查工具,需要下载一个可执行程序:
$ ./validate_m320 --version
validate_m320 version 02.01# 错误时$ cat answer_schema.json
{"_id": "<objectId>","title": "<string>","artist": "<string>","room": "<string>","spot": "<string>","on_display": "<bool>","in_house": "<int>","events": [{"k": "<string>","v": "<date>"}]
}$ ./validate_m320 example --file answer_schema.jsonThe solution is incorrect, use --verbose if you prefer getting some hints$ ./validate_m320 example --file answer_schema.json --verbose
Answer Filename: /home/vagrant/M320/answer_schema.jsonErrors:
in_house: in_house must be one of the following: <bool># 正确时
$ cat answer_schema.json {"_id": "<objectId>","title": "<string>","artist": "<string>","room": "<string>","spot": "<string>","on_display": "<bool>","in_house": "<bool>","events": [{"k": "<string>","v": "<date>"}]
}$ ./validate_m320 example --file answer_schema.json --verbose
Answer Filename: /home/vagrant/M320/answer_schema.json
The document passes validationCongratulations - here is your validation code: 5d124f9bd971a774b97b5fc7
注意,这不是一个JSON检查工具。JSON语法可参见这里。
Handling Duplication, Staleness and Integrity
Handling Duplication
Duplication产生的原因是需要嵌入信息以提供快速访问,主要问题是一致性。
有时,Duplication是更优的方案,例如订单中嵌入客户信息或配送地址,成单时这些信息是固定的,但后续例如配送地址可能会换,因此嵌入在一起是合理的。
还有对于电影和演员这种多对多的关系,一旦电影上映,这些信息不会再变,因此在电影和演员两个collection中使用嵌入而非参照会更合理,有一些重复影响也很小。
以上案例,在document中嵌入的重复信息对于本文档而言不会在改变,后续的变化也不会影响已有的文档。
还有一种重复和预计算有关,例如计算电影票房,需要由应用负责更新,不过更新的频率需参考下一节。
因为有重复,万一这些重复的信息后续需要修改,则需要批量更新。
Handling Staleness
数据陈旧是由于数据变化太快。始终看到最新的数据是无法保证的,关键是看用户能忍受数据的陈旧程度。例如数仓实际针对的都是过去的某一时间点。
为保证数据的新鲜度,一般可采取实时复制或批量更新发,即所谓的涓流式。
Handling Referential Integrity
参照一致性用来连接两个文档,MongoDB不支持cascade deleting(因为不支持主外键)。
可以用多文档事务或change stream解决一致性问题。或者干脆就放在一个文档里。
Attribute Pattern
先来看两个例子。
例一:关于产品的document,有很多共同的属性,如制造商,品牌,价格。还有另外一些属性是各自不同的,但这些属性不多。如果需要查询这些不同的属性,就需要建立太多的索引。因此我们将其装换为子文档,其中k表示属性名,v表示属性值。例如:
从
{
...
"input" : "5V/1300 mA",
"output" : "5V/1A",
"capacity" : "4200 mAh"
}
变为
{
...
"add_specs" : {{ "k" : "input", "b" : "5V/1300 mA"}, { "k" : "output", "b" : "5V/1A"}, { "k" : "capacity", "b" : "4200 mAh"}
}
}
然后针对子文档做索引:
db.products.createIndex({"add_specs.k":1, "add_specs.v":1})
然后就可以有效的处理一下查询:
db.products.find({"add_specs" : {"elementMatch" : {"k" : "capacity", "b" : "4200 mAh"}}})
例2:在文档中的一些属性名字不同,但值的含义类似,例如不同国家的电影上映日期:
{
...
"release_USA":"2020/01/01",
"release_UK":"2020/02/01",
"release_China":"2020/03/01"
}
和例一类似,可以将其转换为array:
"release_date" :
[
{"k":"release_USA", "v":"2020/01/01"},
{"k":"release_UK", "v":"2020/02/01"},
{"k":"release_China", "v":"2020/03/01"}
]
所以Attribute Pattern解决的问题是:
- 有不多的相似的属性
- 需要查询这些属性
解决方法就是将filed:value转换为子文档:
fieldA:field,
fieldB:value
场景包括产品特性,或具有相同value类型的field。
好处是建索引容易,并且可以减少索引。未来添加新的field也能处理,而且容易理解原来的属性关系。可以很好的组织相同属性的field,或不确定的field。
Attribute Pattern是多态的orthogonal(正交),我理解就是90度翻转(Transpose),例如行式变为列式。
Attribute Pattern解决的问题也可用MongoDB 4.2新特性Wildcard Index解决。
注意,在做练习时,一定要看清题意,另外可以通过–verbose获取错误信息,例如:
$ ./validate_m320 pattern_attribute --file pattern_attribute.json --verbose
Answer Filename: /home/vagrant/M320/pattern_attribute.jsonErrors:
(root): Additional property date_acquisition is not allowed
events.0.k: events.0.k must be one of the following: <string>
events.1.k: events.1.k must be one of the following: <string>
这表示以下答案中不会出现具体的值:
"in_house": "<bool>","events": [{ "k": "moma", "v": "<date>"},{ "k": "louvres", "v": "<date>"},{ "k": "date_acquisition", "v": "<date>"}]
而应该是下面的格式
{"events": [{ "k": "<string>","v": "<date>"},{ "k": "<string>","v": "<date>"}]
}
Extended Reference Pattern
Join在关系型数据库中很容易实现,在MongoDB中,Join可以通过以下方法实现:
- 通过应用
- 通过lookup,如
$lookup
和$graphLookup
- 通过嵌入文档避免Join
Extended Reference是指在1-N关系中,在N端嵌入1的数据,如客户与订单,可以在订单中嵌入客户数据,嵌入的只是部分数据,即需要经常Join的数据。因此最终还是两个文档。
嵌入的数据在两端都存在,因此数据有重复。为避免一致性问题,挑选的数据应不经常变化,而且应尽量少。
Extended Reference避免了大量Join,通过将频繁查询的数据嵌入到主文档。例如catalog,移动应用和实时分析。可提供更快的读,减少Join,坏处是数据有重复。
Subset Pattern
Working set应全部容纳在内存中。如果内存容纳不下Working set,会影响性能。
若Working set过大,可以通过纵向扩展或横向扩展(Sharding)添加内存,或减小Working set的大小。
Subset Pattern即将不常用的field移至另一个文档,以减小Working set的大小。例如,对于1-1的field,可直接移到另一文档,对于1-N关系的field,如电影和演员,如果有1000个演员,可以仅保留20个主要演员在常用文档中,而在另一文档中保留所有演员,当然,这样会有数据重复。
Subset Pattern解决的问题是文档过大,导致内存容纳不下,而且文档中大部分的信息不常查询。解决方法是拆成常用和不常用两个文档。
场景包括产品和文章的评价,电影的演员,好处是Working set变小,访问更快,坏处是重复导致的空间占用,少量操作需要应用多轮查询。
Chapter 4: Patterns (Part 2)
Computed Pattern
计算非常消耗资源,因此减少计算对性能有益,例如:
- 数学计算。如统计,例如计算总和,每次插入都需重新计算;通过缓存以前的结果,就可以将多次累加变为一次累加。将中间结果写入另一个document,后台一个进程负责更新,这样实现了读写分离。
- fan out操作。就是一个逻辑操作展开为多个操作,例如fan out 读指需要从多个地方读取数据,fan out写指需要写到多个地方。这样做实际是推送到多个数据需要的地方,用空间换取了性能。
- roll up操作 是drill down操作的反面,例如每小时,每天的统计,可以不断向上卷积。
在计算资源紧张后希望减少读延迟时使用Computed Pattern。
其解决的问题是昂贵的计算操作,频繁在相同数据上计算,并产生相同的结果。解决方法为将计算结果存放于另一个文档。应用场景包括IoT,事件溯源,时间序列数据,聚合框架。好处是读操作更快,节省了计算和磁盘资源。不好在于需求难确定。
Bucket Pattern
Bucket Pattern是每一信息一个文档和一个文档存放所有信息的折衷。实际上也是1-N关系,但N不是所有,而是Total/M(1-Total容纳不下),M的确定需要很好地理解工作负载特征,例如N可能表示每天,或每小时。其实有点像数据库分区,不过分区的大小需要确定。
在文档中存放的某field的信息是以array的信息存放的。类似于列式存储。
使用场景包括IoT,数仓,一个对象包括太多信息。
好处是返回数据和存放空间的平衡。数据更易于管理。删除数据也容易。
坏处是对BI应用不友好,如果设计不当,查询性能不好。
Schema Versioning Pattern
Alter Table NightMare,这几天正好经历过,确实不太方便。但设计上确实应该尽可能精准,以避免后续schema的改动。
关系型数据库修改schema可能需要更新数据,可能需要停机,一旦失败很难回退。
Schema Versioning Pattern的本质是问文档均添加一个版本field,然后应用可以识别版本并做相应处理。所以需要升级应用和迁移文档。
可以避免的问题包括宕机时间,更新文档的耗时,不想更新所有文档。场景包括重度使用的应用,有很多老旧数据的应用。
好处包括无需宕机,迁移可控,无技术债务。
Tree Patterns
树状结构就是层级结构,例如组织架构,目录结构等。在层级结构中经常需要寻找承继关系等操作。
Tree Patterns包括以下4个子模式,这些子模式可以组合:
A. Parent Reference: 存parent,只有一个。更新方便。
B. Child Reference:存children,一到多个,以数组存放
C. Array of Ancestors 数组存放从此节点到根节点(含)间的所有节点,一直向上可以溯源到根。
D. Materialized Paths 以".root.x.y.z…"方式存放节点所有祖先,因此可以使用正则表达式
以下4类问题可以用这些模式解决:
- X的祖先 - C,D
- 谁汇报给Y - A,C
- Z下的所有节点 - B, C?
- 将N下的所有children移到P下 - A
场景包括组织架构,产品分类。
Polymorphic Pattern
Polymorphic Pattern指大部分属性相同,可以归为一类,例如产品。
此Pattern也适合于子文档,例如存各个国家的地址,大部分都相同,但有的国家叫省,有的叫州。
schema versioning pattern也使用了此Pattern,通过版本号,所以此模式是一基础模式。
此模式的实现是通过一个field来跟踪collection的shape,然后应用有不同的处理。
此模式的好处是容易实现,可以提供统一视图。可以将对象放入同一个collection中,因此也可以只在一个collection中查询。
场景包括统一视图,产品目录,内容管理。
Summary of Patterns
Approximation Patterns
近似,就是减少应用到MongoDB的写操作,例如每秒1次更新改为每10秒一次更新。
可以减少计算,但前提是业务可以允许不精确。
实现上是更少的写,但每次写的payload可能更大,起码节省了round-trip。
场景包括Web页面计数器,可以允许不精确(但统计上正确)的计数器,指标统计。
缺点是不精确,需要应用实现。
Outlier Pattern
少数文档会影响整个查询的性能。
实现上兼顾文档中最常用的fields,对于异常文档通过应用单独处理。也就是在主文档中会有标识,异常部分需要到外部去参照。
场景包括社交网络等。
缺点是不适合于即席查询的汇聚操作。
Chapter 5: Conclusion
这一章是考试,有一些小难度。