1、什么是数据模型
数据模型是一组符合、文本组成的集合,用以准确表达信息,达到有效交流、沟通的目的。
比如:保险公司经济人,我们关注的客户主要关注:客户年龄,年收入,家庭成员,工作行业等等
而作为理发店,则主要关注每个月来几次,对发型的需求等等。关注的属性和保险公司所关注的属性完全不同。
2、数据模型设计的元素
2.1、实体Entity
-
描述业务的主要数据集合
-
谁,什么,何时,何地,为何,如何。
2.2、属性 Attribute
属性是进一步用来描述实体的,比如我们看下联系人这个实体,它包含了姓名、公司、职称等等信息。
2.3、关系 Relationship
关系用来描述实体之间的关系,比如一个人包含多个地址,可能家庭住址,公司住址等等。
- 结构规则:1-N;1-1;N-N;
3、传统模型设计:从概念到逻辑到物理
模型
- 概念模型
- 往往是由用户和需求分析来进行讨论,只是粗旷的了解下大体业务逻辑,梳理业务需求
- 逻辑模型
- 基于业务需求,由架构师或者需求分析师,进行实体及实体属性和他们之间的管理罗列出来。
- 物理模型
- 具体实现,通过使用什么数据库,将逻辑模型建立起来。
4、开发者角度下的物理模型
物理模型,主流使用的是关系型模型,主要遵循三范式原则。
遵循三范式的优点:可以减少数据冗余,数据表体积小更新快,范式化的更新操作比 反范式化更快,范式化的表通常比反范式化更小。
缺点:对于查询需要对多个表,会关联多个表,在应用中,进行表关联的成本是很高更难得进行索引优化反范式化设计的优缺点
可以减少表的关联,可以对查询更好的进行索引优化,缺点,表结构存在数据冗余和数据维护异常,对数据的修改需要更多资源。
因此在设计数据库结构的时候要将反范式化和范式化结合起来。
二、JSON文档模型设计
2.1、MongoDB文档模型设计的三个误区
- 不需要模型设计
- MongoDB应该用一个超级大文档来组织所有数据
- MongoDB不支持关联或事物
以上这些说法全是错误的。
2.2、关于JOSN文档模型设计
文档模型设计处于是物理模型设计阶段 (PDM)
JSON 文档模型通过内嵌数组或引用字段来表示关系
文档模型设计不遵从第三范式,允许冗余。
严格来说,MongoDB 同样需要概念/逻辑建模
文档模型设计的物理层结构可以和逻辑层类似
2.3、逻辑模型 – JSON 模型
下面是一个JSON模型和逻辑模型的对比, 这里json文档中的联系人,列举出了姓名、性别、创建日期、所属组、和地址;
这样一个物理关系模型中需要多张表,而这里可以直接通过一个json来表述出来。
2.4、文档模式设计原则:性能和易用
在文档模式设计中我们是没有第三范式原则的,这是个好处也是个弊端,很多人不知道该如何设计这个模型。什么样的模型好什么样的模型坏。
我们是有两个关键点,看你这个模型是否性能不错,能够支撑高并发、低延迟的读写。另外一个角度在程序开发过程中是否简易。
这里没有唯一的标准,这里更多的是经验之谈。我们后面给大家些建模的建议。可以尊从文档模式设计三步走:
三、文档模式设计:基础建模
-
根据概念模型或者业务需求推导出逻辑模型 – 找到对象
-
列出实体之间的关系(及基数) - 明确关系
-
套用逻辑设计原则来决定内嵌方式 – 进行建模
-
完成基础模型构建
3.1、1:1关系建立
比如,一个联系人只有一个头像,
- 基本原则:一对一关系以内嵌为主作为子文档形式 或者直接在顶级不涉及到数据冗余;
- 例外情况:如果内嵌后导致文档大小超过16MB
1{
2 "name": "TJ Tang",
3 "company": "TAPDATA",
4 "title": " CTO",
5 "portraits": {
6 "mimetype": "xxx",
7 "data": "xxxx"
8 }
9}
3.2、一对多关系建立
比如一个联系人可以有多个地址:
- 一对多关系同样以内嵌为主用数组来表示一对多不涉及到数据冗余;
- 例外情况:如果内嵌后导致文档大小超过16MB,数组长度太大(数万或更多),数组长度不确定;
1{
2 "name": "TJ Tang",
3 "company": "TAPDATA",
4 "title": " CTO",
5 "portraits": {
6 "mimetype": "xxx",
7 "data": "xxxx"
8 },
9 "addresses": [
10 {
11 "type": "home",
12 ....
13 },
14 {
15 "type": "work",
16 ....
17 }
18 ]
19}
3.3、多对多关系建模
比如一个联系人可以有多个地址:
- 一对多关系同样以内嵌为主用数组来表示一对多,通过冗余来实现N-N;
- 例外情况:如果内嵌后导致文档大小超过16MB,数组长度太大(数万或更多),数组长度不确定;
1{
2 "name": "TJ Tang",
3 "company": "TAPDATA",
4 "title": " CTO",
5 "portraits": {
6 "mimetype": "xxx",
7 "data": "xxxx"
8 },
9 "addresses": [
10 {
11 "type": "home",
12 ....
13 },
14 {
15 "type": "work",
16 ....
17 }
18 ],
19 "groups":[
20 {"name":"FRI"},
21 {"name":"DF"},
22 ]
23}
四、文档模式设计:工况细化
在做工况细化过程中,我们需要根据技术需求对我们模型进行调整,这是个技术导向,我们需要和业务方进行详细的沟通,了解到数据的使用方法,是根据什么来查询的。如:单个查询,报表查询,查询参数是什么、数量有多大、读写比例有多少等等。针对不同需求,我们可以引入引用、关联、或者冗余等手段来解决这些问题。
● 最频繁的数据查询模式
● 最常用的查询参数
● 最频繁的数据写入模式
● 读写操作的比例
● 数据量的大小
比如:联系人管理应用的分组需求
4.1 解决方案:Group 使用单独的集合
类似于关系型设计,使用 id 或者唯一键关联,使用
4.2、联系人的头像: 引用模式
头像使用高保真,大小在 5MB-10MB
头像一旦上传,一个月不可更换
基础信息查询(不含头像)和 头像查询的比例为 9 :1
建议: 使用引用方式,把头像数据放到另外一个集合,可以显著提升 90% 的查询效率。
4.3、什么时候使用引用模式
MongoDB 引用设计的限制
-
内嵌文档太大,数 MB 或者超过 16MB
-
内嵌文档或数组元素会频繁修改
-
内嵌数组元素会持续增长并且没有封顶
MongoDB 引用设计的限制
-
MongoDB 对使用引用的集合之间并无主外键检查
-
MongoDB 使用聚合框架的 $lookup 来模仿关联查询
-
$lookup 只支持 left outer join
-
$lookup 的关联目标(from)不能是分片表
五、文档设计模式:套用设计模式
文档模型:无范式,无思维定式,充分发挥想象力;
设计模式:实战过屡试不爽的设计技巧,快速应用;
举例:一个IoT场景的分桶设计模式,可以帮助把存储空间降低10倍并且查询效率提升数十倍。
问题:物联网场景下的海量数据处理 - 设备监控数据
上报数据格式如下:
1{
2 "_id":"10000000022200000222:CA2091",
3 "icao":"CA2091",
4 "ts":ISODate("2022-04-03T20:21:35.000+0000"),
5 "events":{
6 "tem":35.3,
7 "humidity":50.3,
8 "lon":38.345,
9 "lat":58.987,
10 "open":"0",
11 "b":"b"
12 "p":[123,245],
13 "s":91
14 }
15}
我们假设有10万个设备,每分钟上报一条数据,一年的数据量大约52560*100W = 525亿数据,约10TB;
每分钟1条 | 计算说明 | |
---|---|---|
文档条数 | 52.5B | 10W * 365 * 24 * 60 |
索引大小 | 6364GB | 10W * 365 * 24 * 60 * 130 |
_id index | 1468GB | |
{ts:1,icao:1} | 4895GB | |
文档平均大小 | 92Bytes | |
数据大小 | 4503GB | 10W * 365 * 24 * 60 * 92 |
5.2、解决方案-分桶设计
思路将之前每分钟数据汇总到每小时一条,将每分钟数据存放在events中,变更结构如下:
1{
2 "_id": "10000000022200000222:CA2091",
3 "icao": "CA2091",
4 "ts": ISODate("2022-04-03T20:00:00.000+0000"),//每小时
5 "events": [//每小时60条数据,集合条数固定
6 {
7 "tem": 35.3,
8 "humidity": 50.3,
9 "lon": 38.345,
10 "lat": 58.987,
11 "open": "0",
12 "b": "b",
13 "p": [
14 123,
15 245
16 ],
17 "s": 91,
18 "ts": ISODate("2022-04-03T20:01:00.000+0000")//每分钟
19 },
20 {
21 "tem": 35.3,
22 "humidity": 50.3,
23 "lon": 38.345,
24 "lat": 58.987,
25 "open": "0",
26 "b": "b",
27 "p": [
28 123,
29 245
30 ],
31 "s": 91,
32 "ts": ISODate("2022-04-03T20:02:00.000+0000")
33 }
34 ]
35}
这里我们一个文档可以存储一个小时的设备数据,而events集合中的数据条数是固定的,每小时60条。
可视化表现 24 小时的设备数据,查询1440 次读操作即可。
指标 | 每分钟1条 | 每小时一个文档 |
---|---|---|
文档条数 | 52.5B | 876 M |
索引大小 | 6364GB | 106 GB |
_id index | 1468GB | 24.5 GB |
{ts:1,icao:1} | 4895GB | 81.6 GB |
文档平均大小 | 92Bytes | 758 Bytes |
数据大小 | 4503GB | 618 GB |
分桶方式总结:
场景 | 痛点 | 设计模式方案及优点 |
---|---|---|
时序数据 | 数据点采集频繁,数据量太多 | 利用文档内嵌数组,将一个时间段的数据聚合到一个文档里 |
物联网 | 大量减少文档数量 | |
智慧城市 | 大量减少索引占用空间 | |
智慧交通 | ||
六、设计模式集锦
6.1、列转行模式
问题: 大文档,很多字段,很多索引
screenshot-20220409-215935
解决方案:列转行
- 列转行总结:
场景 | 痛点 | 设计模式方案及优点 |
---|---|---|
产品属性 ‘color’, ‘size’, ‘dimensions’, …物联网,多语言(多国家)属性 | 文档中有很多类似的字段,会用于组合查询搜索,需要建很多索引 | 转化为数组,一个索引解决所有查询问题 |
6.2、版本字段
问题:模型灵活了,如何管理文档不同版本?
修改前版本:
1{
2 "_id": ObjectId("5de26f197edd62c5d388babb"),
3 "name": "TJ",
4 "company": "Tapdata"
5}
新增手机号后版本:
1{
2 "_id": ObjectId("5de26f197edd62c5d388babb"),
3 "name": "TJ",
4 "company": "Tapdata",
5 "phone":"182XXXX8888"
6}
解决方案:增加一个版本字段:
1{
2 "_id": ObjectId("5de26f197edd62c5d388babb"),
3 "name": "TJ",
4 "company": "Tapdata",
5 "phone":"182XXXX8888",
6 "schema_version": 2.0
7}
- 版本字段总结:
场景 | 痛点 | 设计模式方案及优点 |
---|---|---|
任何有版本衍变的数据库 | 文档模型格式多,无法知道其合理性,升级时候需要更新太多文档 | 增加一个版本号字段;快速过滤掉不需要升级的文档;升级时候对不同版本的文档做不同的处理 |
6.3、近似计算
问题:统计网页点击流量
- 解决方案: 用近似计算
每隔10 (X)次写一次,
- 近似计算总结:
场景 | 痛点 | 设计模式方案及优点 |
---|---|---|
网页计数;各种结果不需要准确的排名; | 写入太频繁,消耗系统资源 | 间隔写入,每隔10次或者100次,大量减少写入需求 |
6.4、使用预聚合字段
问题: 业绩排名,游戏排名,商品统计等精确统计
热销榜:某个商品今天卖了多少,这个星期卖了多少,这个月卖了多少?
电影排行:观影者,场次统计
传统解决方案:通过聚合计算
痛点:消耗资源多,聚合计算时间长
- 解决方案: 用预聚合字段
在模型中新增预聚合字段,每次更新数据的时候同步更新统计值。
1{
2 "product": "Bike",
3 "sku": "abc123456",
4 "quantitiy": 20394,
5 "daily_sales": 40,
6 "weekly_sales": 302,
7 "monthly_sales": 1419
8}
1db.inventory.update({_id:123},
2{$inc: {
3 quantity: -1,
4 daily_sales: 1,
5 weekly_sales: 1,
6 monthly_sales: 1,
7 }
8})
- 预聚合字段总结:
场景 | 痛点 | 设计模式方案及优点 |
---|---|---|
准确排名,排行榜 | 统计计算耗时,计算时间长 | 模型中直接增加统计字段;每次更新数据时候同时更新统计值 |
注:以上内容参考《MongoDB 高手课》