|
|
|
|
## 数据模型设计
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 模型设计基础
|
|
|
|
|
|
|
|
|
|
- 数据模型设计的元素
|
|
|
|
|
- 实体
|
|
|
|
|
- 描述业务的主要数据集合
|
|
|
|
|
- 谁, 什么, 何时, 何地, 为何, 如何
|
|
|
|
|
- 属性
|
|
|
|
|
- 描述实体里面的单个信息
|
|
|
|
|
- 关系
|
|
|
|
|
- 描述实体与实体之间的数据规则
|
|
|
|
|
- 结构规则: 1-N, N-1, N-N
|
|
|
|
|
- 引用规则: 电话号码不能单独存在
|
|
|
|
|
|
|
|
|
|
- 传统模型设计
|
|
|
|
|
- ![传统模型设计.png](pic/传统模型设计.png)
|
|
|
|
|
- 架构师要产出 LDM 给开发者
|
|
|
|
|
- 逻辑模型
|
|
|
|
|
- 实体, 属性, 函数名称, 实体间关系
|
|
|
|
|
- 开发者: 第三范式下的物理模型
|
|
|
|
|
- 数据在库里尽量不可能存在冗余
|
|
|
|
|
|
|
|
|
|
### 2. JSON 文档模型的设计特点
|
|
|
|
|
|
|
|
|
|
- MongoDB 文档模型设计的三个误区
|
|
|
|
|
- 不需要模型设计
|
|
|
|
|
- MongoDB 应用用一个超级大文档来组织所有数据
|
|
|
|
|
- MongoDB 不支持关联或者事务
|
|
|
|
|
|
|
|
|
|
- 关于 JSON 文档模型设计
|
|
|
|
|
- 文档模型设计处于是物理模型设计阶段 (PDM)
|
|
|
|
|
- JSON 文档模型通过内嵌数组或引用字段来表示关系
|
|
|
|
|
- 文档模型设计不遵从第三范式, 允许冗余
|
|
|
|
|
- 流程: 概念建模 -> 逻辑建模 -> 文档模型 / 关系模型
|
|
|
|
|
|
|
|
|
|
- 为什么人们都说 MongoDB 是无模式
|
|
|
|
|
- 严格来说, MongoDB 同样需要概念/逻辑建模
|
|
|
|
|
- 文档建模设计的物理层结构可以和逻辑层类似
|
|
|
|
|
- MongoDB 无模式由来:
|
|
|
|
|
- **可以省略物理建模的具体过程**
|
|
|
|
|
- 概念模型 -> 逻辑模型 -(复用)-> 物理模型
|
|
|
|
|
|
|
|
|
|
- JSON 模型和逻辑模型对比
|
|
|
|
|
- ![JSON 模型和逻辑模型对比.png](pic/JSON%20模型和逻辑模型对比.png)
|
|
|
|
|
|
|
|
|
|
- **文档模型的设计原则: 性能和易用**
|
|
|
|
|
|
|
|
|
|
- 关系模型 vs 文档模型
|
|
|
|
|
- ![关系模型 vs 文档模型.png](pic/关系模型%20vs%20文档模型.png)
|
|
|
|
|
|
|
|
|
|
### 3. 文档模型三步走
|
|
|
|
|
|
|
|
|
|
- MongoDB 文档模型设计三步曲 - 总览
|
|
|
|
|
- ![MongoDB文档模型设计三步曲.png](pic/MongoDB文档模型设计三步曲.png)
|
|
|
|
|
|
|
|
|
|
- **第一步: 建立基础文档模型**
|
|
|
|
|
- 根据概念模型或者业务需求推导出逻辑模型 - 找到对象
|
|
|
|
|
- 列出实体之间的关系(及基数) - 明确关系
|
|
|
|
|
- 套用逻辑设计原则来决定内嵌方式 - 进行建模
|
|
|
|
|
- 完成基础模型构建
|
|
|
|
|
- 业务需求及逻辑模型 -(逻辑导向)-> 基础建模 -> 集合/字段/基础形状
|
|
|
|
|
|
|
|
|
|
- 举例
|
|
|
|
|
- 找到对象:
|
|
|
|
|
- Contacts
|
|
|
|
|
- Groups
|
|
|
|
|
- Address
|
|
|
|
|
- Portraits
|
|
|
|
|
- 明确关系
|
|
|
|
|
- 一个联系人有一个头像(1-1)
|
|
|
|
|
- 一个联系人可以有多个地址(1-N)
|
|
|
|
|
- 一个联系人可以属于多个组, 一个组可以有多个联系人(N-N)
|
|
|
|
|
- 表示:
|
|
|
|
|
- Groups(name) <-[N-N]-> Contacts (name,company,title) <-[1-N]-> Addresses(type,street,city,state,zip_code)
|
|
|
|
|
- Contacts(...) <-[1-1]-> Portraits(mine_type,date)
|
|
|
|
|
|
|
|
|
|
- 1-1 关系建模: portraits
|
|
|
|
|
- 基本原则: **一对一关系以内嵌为主, 作为子文档形式或者直接在顶级不涉及到数据冗余**
|
|
|
|
|
- 例外情况: **如果内嵌后导致文档大小超过16MB**
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
name: "TJ Tang",
|
|
|
|
|
company: "TAPDATA",
|
|
|
|
|
title: "CTO",
|
|
|
|
|
portraits: {
|
|
|
|
|
mimetype: xxx,
|
|
|
|
|
date: xxxx
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- 1-N 关系建模: Addresses
|
|
|
|
|
- 基本原则: **一对多关系同样以内嵌为主, 用数组来表示一对多不涉及到数据冗余**
|
|
|
|
|
- 例外情况: **内嵌后导致文档大小超过 16 MB, 数组长度太大(数万或更多) 数组长度不确定**
|
|
|
|
|
- Groups(name) <-[N-N]->
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
name: "TJ Tang",
|
|
|
|
|
company: "TAPDATA",
|
|
|
|
|
title: "CTO",
|
|
|
|
|
portraits: {
|
|
|
|
|
mimetype: xxx,
|
|
|
|
|
date: xxxx
|
|
|
|
|
},
|
|
|
|
|
addresses: {
|
|
|
|
|
{type: home, ...},
|
|
|
|
|
{type: work, ...},
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- N-N 关系建模: 内嵌数组模式
|
|
|
|
|
- 基本原则: **不需要映射, 一般用内嵌数组来表示一对多, 通过冗余来实现 N-N**
|
|
|
|
|
- 例外情况: **内嵌后导致文档大小超过 16 MB, 数组长度太大(数万或更多) 数组长度不确定**
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
name: "TJ Tang",
|
|
|
|
|
company: "TAPDATA",
|
|
|
|
|
title: "CTO",
|
|
|
|
|
portraits: {
|
|
|
|
|
mimetype: xxx,
|
|
|
|
|
date: xxxx
|
|
|
|
|
},
|
|
|
|
|
addresses: {
|
|
|
|
|
{type: home, ...},
|
|
|
|
|
{type: work, ...},
|
|
|
|
|
},
|
|
|
|
|
groups: {
|
|
|
|
|
{name: :"Friends"}
|
|
|
|
|
{name: :"Surfers"}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- **第二步: 根据读写工况细化**
|
|
|
|
|
- 技术需求, 读写比例, 方式及数量 -(技术导向)-> 工况细化 -> [引用及关联]
|
|
|
|
|
- 考虑的问题
|
|
|
|
|
- 最频繁的数据查询模式
|
|
|
|
|
- 最常用的查询参数
|
|
|
|
|
- 最频繁的数据写入模式
|
|
|
|
|
- 读写操作的比例
|
|
|
|
|
- 数据量的大小
|
|
|
|
|
- 基于内嵌的文档模型
|
|
|
|
|
- 根据业务需求
|
|
|
|
|
- 使用引用来避免性能瓶颈
|
|
|
|
|
- 使用冗余来优化访问性能
|
|
|
|
|
|
|
|
|
|
- 举例
|
|
|
|
|
- 联系人管理应用分组需求
|
|
|
|
|
- 用于客户营销
|
|
|
|
|
- 有千万级联系人
|
|
|
|
|
- 需要频繁变动分组的信息, 如增加分组及修改名称及描述以及营销状态
|
|
|
|
|
- 一个分组可以有百万级联系人
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
name: "TJ Tang",
|
|
|
|
|
company: "TAPDATA",
|
|
|
|
|
title: "CTO",
|
|
|
|
|
portraits: {
|
|
|
|
|
mimetype: xxx,
|
|
|
|
|
date: xxxx
|
|
|
|
|
},
|
|
|
|
|
addresses: {
|
|
|
|
|
{type: home, ...},
|
|
|
|
|
{type: work, ...},
|
|
|
|
|
},
|
|
|
|
|
groups: {
|
|
|
|
|
{name: :"Friends"}
|
|
|
|
|
{name: :"Surfers"}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
```json
|
|
|
|
|
name: "Mona Jang",
|
|
|
|
|
company: "HUAXIA",
|
|
|
|
|
title: "DIRECTOR",
|
|
|
|
|
portraits: {
|
|
|
|
|
mimetype: xxx,
|
|
|
|
|
date: xxxx
|
|
|
|
|
},
|
|
|
|
|
addresses: {
|
|
|
|
|
{type: home, ...},
|
|
|
|
|
{type: work, ...},
|
|
|
|
|
},
|
|
|
|
|
groups: {
|
|
|
|
|
{name: :"Friends"}
|
|
|
|
|
{name: :"Surfers"}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 一个分组(groups)的改动意味着百万级的DB操作
|
|
|
|
|
- 解决方案: Group 使用单独的集合
|
|
|
|
|
- 类似于关系型设计
|
|
|
|
|
- 用 id 或者唯一键关联
|
|
|
|
|
- 使用 $lookup 来提供一次查询多表的能力(类似关联)
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
name: "Mona Jang",
|
|
|
|
|
company: "HUAXIA",
|
|
|
|
|
title: "DIRECTOR",
|
|
|
|
|
portraits: {
|
|
|
|
|
mimetype: xxx,
|
|
|
|
|
date: xxxx
|
|
|
|
|
},
|
|
|
|
|
addresses: {
|
|
|
|
|
{type: home, ...},
|
|
|
|
|
{type: work, ...},
|
|
|
|
|
},
|
|
|
|
|
group_ids: [1,2,3]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
Groups
|
|
|
|
|
group_id
|
|
|
|
|
name
|
|
|
|
|
```
|
|
|
|
|
- 使用 group_id 进行关联
|
|
|
|
|
- 此时查询语句
|
|
|
|
|
```spring-mongodb-json
|
|
|
|
|
db.contacts.aggregate([
|
|
|
|
|
{
|
|
|
|
|
$lookup:{
|
|
|
|
|
from: "groups",
|
|
|
|
|
localField: "group_ids",
|
|
|
|
|
foreignField: "group_id",
|
|
|
|
|
as: "groups"
|
|
|
|
|
}
|
|
|
|
|
}])
|
|
|
|
|
```
|
|
|
|
|
- aggregate 的 $lookup 来实现关联查询
|
|
|
|
|
|
|
|
|
|
- 例子2: 联系人的头像: 引用模式
|
|
|
|
|
- 头像使用高保真, 大小在 5MB - 10MB
|
|
|
|
|
- 头像一旦上传, 一个月不可变更
|
|
|
|
|
- 基础信息查询(不含头像) 和 头像查询的比例为 9 : 1
|
|
|
|
|
- 建议: 使用引用方式, 把头像数据放到另外一个集合, 可以显著提升 90% 的查询效率
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
name: "Mona Jang",
|
|
|
|
|
company: "HUAXIA",
|
|
|
|
|
title: "DIRECTOR",
|
|
|
|
|
addresses: {
|
|
|
|
|
{type: home, ...},
|
|
|
|
|
{type: work, ...},
|
|
|
|
|
},
|
|
|
|
|
group_ids: [1,2,3]
|
|
|
|
|
```
|
|
|
|
|
```json
|
|
|
|
|
Contact_Portrait: {
|
|
|
|
|
_id: 123,
|
|
|
|
|
mimetype: xxx,
|
|
|
|
|
data: xxxx
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- 什么时候该使用引用方式?
|
|
|
|
|
- 内嵌文档太大, 数 MB 或者超过 16MB
|
|
|
|
|
- 内嵌文档或数组元素会频繁修改
|
|
|
|
|
- 内嵌数组元素会持续增长并且没有封顶
|
|
|
|
|
|
|
|
|
|
- MongoDB 引用设计的限制
|
|
|
|
|
- MongoDB 对使用引用的集合之间并无主外键检查
|
|
|
|
|
- MongoDB 使用聚合框架的 $lookup 来模仿关联查询
|
|
|
|
|
- $lookup 只支持 left outer join
|
|
|
|
|
- $lookup 的关联目标(from) 不能是分片表
|
|
|
|
|
|
|
|
|
|
- **第三步: 套用设计模式**
|
|
|
|
|
- 文档模式: 无范式, 无思维定式, 充分发挥想象力
|
|
|
|
|
- 设计模式: 实战过屡试不爽的设计技巧, 快速应用
|
|
|
|
|
- 举例: 一个 loT 场景的分桶设计模式, 可以帮助把存储空间降低10倍并且查询效率提升数十倍
|
|
|
|
|
- 经验和学习 -(模式导向)-> 套用设计模式 -> 优化的模型
|
|
|
|
|
|
|
|
|
|
- 问题: 物联网场景下的海量数据处理 - 飞机监控数据
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"_id" : "20160101050000:CA2790",
|
|
|
|
|
"icao": "CA2790",
|
|
|
|
|
"callsign": "CA2790",
|
|
|
|
|
"ts": ISODate("2016-01-01T05:00:00.000+0000"),
|
|
|
|
|
"events": {
|
|
|
|
|
"a": 31418,
|
|
|
|
|
"b" : 173,
|
|
|
|
|
"s" :91,
|
|
|
|
|
"v" : 80
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 实际问题: 520亿条, 10TB - 海量数据
|
|
|
|
|
- 10万架飞机
|
|
|
|
|
- 1年的数据
|
|
|
|
|
- 每分钟1条
|
|
|
|
|
- ![飞机-海量数据.png](pic/飞机-海量数据.png)
|
|
|
|
|
- 52.6B = 100000 * 365 * 24 * 60
|
|
|
|
|
- 6364GB = 100000 * 365 * 24 * 60 * 130
|
|
|
|
|
- 4503GB = 100000 * 365 * 24 * 60 * 92
|
|
|
|
|
|
|
|
|
|
- 解决方案: 分桶设计
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
" id" :"20160101050000:WG9943",
|
|
|
|
|
"icao":"WG9943",
|
|
|
|
|
"ts":ISODate("2016-01-01T05:00:00.000+0000"),
|
|
|
|
|
"events":[
|
|
|
|
|
{
|
|
|
|
|
"a":24293, "b":319, "p":[41,70], "s":56,
|
|
|
|
|
"t": ISODate("2016-01-01T05:00:00.000+0000")
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"a":33663, "b":134, "p":[-38, -30], "s":385,
|
|
|
|
|
"t": ISODate("2016-01-01T05:00:01.000+0000")
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 60 events == 1小时的数据
|
|
|
|
|
- 一个文档: 一架飞机一个小时的数据
|
|
|
|
|
- ![飞机-海量数据-分桶.png](pic/飞机-海量数据-分桶.png)
|
|
|
|
|
- 可视化表现24小时的飞行数据
|
|
|
|
|
- 1440次读
|
|
|
|
|
|
|
|
|
|
- 模式小结:
|
|
|
|
|
- 场景
|
|
|
|
|
- 时序数据
|
|
|
|
|
- 物联网
|
|
|
|
|
- 智慧城市
|
|
|
|
|
- 智慧交通
|
|
|
|
|
- 痛点
|
|
|
|
|
- 数据点采频繁, 数据量太多
|
|
|
|
|
- 设计模式的方案及优点
|
|
|
|
|
- 利用文档内嵌数组, 将一个时间段的数据聚合到一个文档里
|
|
|
|
|
- 大量减少文档数量
|
|
|
|
|
- 大量减少索引占用空间
|
|
|
|
|
|
|
|
|
|
### 4. 设计模式集锦
|
|
|
|
|
|
|
|
|
|
- 问题: 大文档, 很多字段, 很多索引
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
title: "Dunkirk",
|
|
|
|
|
release USA:"2017/07/23",
|
|
|
|
|
release UK:"2017/08/01",
|
|
|
|
|
release France:"2017/08/01",
|
|
|
|
|
release Festival San Jose:"2017/07/22"
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 需要很多索引
|
|
|
|
|
```json
|
|
|
|
|
{release_UsA:1}
|
|
|
|
|
{release_UK:1}
|
|
|
|
|
{release_France:1}
|
|
|
|
|
{release_Festival_San_Jose:1 }
|
|
|
|
|
```
|
|
|
|
|
- 电影的上映时间
|
|
|
|
|
|
|
|
|
|
- 解决方法: 列转行
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
title: "Dunkirk",
|
|
|
|
|
releases:[
|
|
|
|
|
{country:"USA",date:"2017/7/23"},
|
|
|
|
|
{country:"Uk",date:"2017/08/01”}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 转换后: 字段数变少了
|
|
|
|
|
- 创建索引: db.movies.createIndex({"releases.country":1, "releases.date":1})
|
|
|
|
|
|
|
|
|
|
- 模式小结:
|
|
|
|
|
- 场景: 产品属性多, 多语言(多国家属性)
|
|
|
|
|
- 痛点: 文档中有很多类似的字段, 会用于组合查询搜索, 需要创建很多索引
|
|
|
|
|
- 设计模式方案及优点: 转化为数组, 一个索引解决所有查询问题
|
|
|
|
|
|
|
|
|
|
- 问题: 模型灵活了, 如何管理文档的不同版本
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
"id":ObiectId("5de26f197edd62c5d388babb"),
|
|
|
|
|
"name":"TJ",
|
|
|
|
|
"Tapdata""company":
|
|
|
|
|
```
|
|
|
|
|
- v1.0
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
"id":obiectId("5de26f197edd62c5d388babb")
|
|
|
|
|
"name":"TJ"
|
|
|
|
|
"company":"Tapdata"
|
|
|
|
|
"wechat":"titang826"
|
|
|
|
|
"schema_version": "2.0"
|
|
|
|
|
```
|
|
|
|
|
- v2.0
|
|
|
|
|
- 解决方案:schema_version 使用这个字段进行区分标识
|
|
|
|
|
|
|
|
|
|
- 模式小结:版本字段
|
|
|
|
|
- 场景: 任何有版本衍生的数据库
|
|
|
|
|
- 痛点: 文档模型格式多, 无法知道其合理性, 升级时候需要更新太多文档
|
|
|
|
|
- 设计模式及优点: 增加一个版本号字段, 快速过滤掉不需要升级的文档, 升级时候对不同的文档做不同的升级
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- 问题: 统计网页点击流量
|
|
|
|
|
- 每访问一个页面就会产生一次数据库计数更新操作
|
|
|
|
|
- 统计数字准确性并不十分重要
|
|
|
|
|
|
|
|
|
|
- 解决方案: 用近似计算
|
|
|
|
|
- 每隔 10(X)次写一次
|
|
|
|
|
- Increment by 10(X)
|
|
|
|
|
- {$inc:{views:1}}
|
|
|
|
|
- if random(0,9) == 0 increment by 10
|
|
|
|
|
|
|
|
|
|
- 模式小结: 近似计算
|
|
|
|
|
- 场景:
|
|
|
|
|
- 网页计数
|
|
|
|
|
- 各种结果不需要准确的排名
|
|
|
|
|
- 痛点:
|
|
|
|
|
- 写入太频繁, 消耗系统资源
|
|
|
|
|
- 设计模式方案及优点
|
|
|
|
|
- 间隔写入, 每隔10次或者100次
|
|
|
|
|
- 大量减少写入需求
|
|
|
|
|
|
|
|
|
|
- 问题: 业绩排名, 游戏排名, 商品统计等精确统计
|
|
|
|
|
- 热销榜: 某个商品今天卖了多少, 这个星期卖了多少, 这个月卖了多少?
|
|
|
|
|
- 电影排行: 观影者, 场次统计
|
|
|
|
|
- 传统解决方案: 通过聚合计算
|
|
|
|
|
- 痛点: 消耗资源多, 聚合计算时间长
|
|
|
|
|
|
|
|
|
|
- 解决方案: 用预聚合字段
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
product:"Bike"
|
|
|
|
|
sku:“abc123456”
|
|
|
|
|
quantitiy:20394,
|
|
|
|
|
daily_sales: 40,
|
|
|
|
|
weekly sales:302,
|
|
|
|
|
monthly_sales:1419
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
```spring-mongodb-json
|
|
|
|
|
db.inventory.update({_id:123},{
|
|
|
|
|
$inc:{
|
|
|
|
|
quantity:-1,
|
|
|
|
|
daily_sales: 1,
|
|
|
|
|
weekly_sales:1,
|
|
|
|
|
monthly_sales: 1
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- 模式小结: 预聚合
|
|
|
|
|
- 场景: 准确排名, 排行榜
|
|
|
|
|
- 痛点: 统计计算耗时, 计算时间长
|
|
|
|
|
- 设计模式方案及优点: 模型中直接增加统计字段, 每次更新数据时候同时更新统计值
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|