模型设计基础
数据模型的三要素:
实体Entity:描述业务的主要数据集合
邮箱、电话、地址
属性Attribute:描述实体里面的单个信息
邮箱下的类型和地址、电话类型和号码、地址下的省、市、县等
关系Relationship:描述实体间的数据规则,结构规则:1-N,N-1,N-N
一个联系人可以有一个头像(1-1)
一个联系人可以有多个地址(1-N)
一个联系人可以属于多个组,一个组可以有多个联系人(N-N)
数据模型的三层深度:
概念模型CDM | 逻辑模型LDM | 物理模型PDM | |
目的 | 描述业务系统要管理的对象 | 基于概念模型,详细列出所有实体、实体的属性及关系 | 根据逻辑模型,结合数据库的物理结构,设计具体的表结构,字段列表及主外键 |
特点 | 用概念名词来描述现实中的实体及业务规则,如“联系人” | 基于业务的描述,和数据库无关 | 技术实现细节,和具体的数据库类型相关 |
主要使用者 | 用户需求分析师 | 需求分析师、架构师、开发者 | 开发者、数据库管理员(DBA) |
关系模型的设计原则——第三范式原则:
数据在库里尽量不可能存在冗余
JSON文档模型设计
文档模型设计处于物理模型设计PDM,不像关系模型需要遵从第三范式,允许冗余
设计原则:
性能:是否能支撑高并发,低延迟的读写
开发易用:程序开发中是否容易使用
关系数据库 | JSON文档模型 | |
模型设计层次 | 概念模型→逻辑模型→物理模型 | 概念模型→逻辑模型 |
模型实体 | 表 | 集合 |
模型属性 | 列 | 字段 |
模型关系 | 关联关系,主外键 | 内嵌数组,引用字段 |
文档模型设计三步:
graph LR
A1([业务需求<br>逻辑模型])
A2(基础建模)
A3("集合<br>字段<br>基础形状")
B1([技术需求<br>读写比例<br>方式及数量])
B2(工况细化)
B3(引用及关联)
C1([经验和学习])
C2(套用设计模式)
C3(最终模式)
A1 --逻辑导向--> A2 --> A3
B1 --技术导向--> B2 --> B3
C1 --模式导向--> C2 --> C3
基础建模
graph LR
A1([业务需求<br>逻辑模型])
A2(基础建模)
A3("集合<br>字段<br>基础形状")
A1 --逻辑导向--> A2 --> A3
根据概念模型/业务需求推导出逻辑模型→找到对象
列出实体间的关系(及基数)→明确关系
套用逻辑设计原则来决定内嵌方式→进行建模
完成基础模型构建
1-1关系建模:以内嵌为主,作为子文档形式或直接在顶级
例外:
内嵌后导致文档大小超过16MB
1-N关系建模:以内嵌为主
例外:
内嵌后导致文档大小超过16MB
数组长度太大(数万↑)
数组长度不确定
N-N关系建模:不需要映射表,用内嵌数组表示一对多,通过冗余来实现N-N
例外:
内嵌后导致文档大小超过16MB
数组长度太大(数万↑)
数组长度不确定
工况细化
graph LR
B1([技术需求<br>读写比例<br>方式及数量])
B2(工况细化)
B3(引用及关联)
B1 --技术导向--> B2 --> B3
根据下述问题细化:
最频繁的数据查询模式
最常用的查询参数
最频繁的数据写入模式
读写操作的比例
数据量的大小
引用:避免性能瓶颈
何时使用?
内嵌文档太大,数组MB或超过16MB
例:用户信息存在集合Contacts中,通过引用的方式获取存在集合Contact_Portrait中的头像。在用户查询时先查询基础信息,点击查看头像时再根据引用查询头像,提高查询效率
内嵌文档/数组数据会频繁修改
内嵌数组会持续增长并且没有封顶
引用设计的限制
使用引用的集合间没有主外键检查
使用聚合框架的$lookup来模仿关联查询
$lookup只支持left outer join
$lookup的关联目标(from)不能是分片表
冗余:优化访问性能
模式套用
graph LR
C1([经验和学习])
C2(套用设计模式)
C3(最终模式)
C1 --模式导向--> C2 --> C3
一个好的设计模式可以:
提升数据读写效率
降低资源需求
设计模式集锦
列转行
某个电影需要记录不同国家的发布时间,需要很多字段和索引
{
title: "Dunkirk",
...
release_USA: "2017/07/23",
release_UK: "2017/08/01",
release_France: "2017/08/01",
release_Festival_San_Jose:
"2017/07/22"
}
使用列转行:
字段数据变少
通过一个索引支持所有国家的查询
db.movies.createIndex({“releases.country”:1, “releases.date”:1})
{
title: "Dunkirk",
...
releases: [
{ country: “USA”, date:”2017/07/23”},
{ country: “UK”, date:”2017/08/01”}
]
}
场景 | 痛点 | 设计模式方案及优点 |
产品属性'color','size'... 多语言(多国家)属性 | 文档中有很多类似的字段 会用于组合查询搜索,需要建很多索引 | 转化为数组,一个索引解决所有查询问题 |
版本字段
版本二的文档比版本一的文档多了一个字段,可以通过增加一个版本字段的方式管理文档
场景 | 痛点 | 设计模式方案及优点 |
任何有版本衍变的数据库 | 文档模型格式多,无法知其合理性 升级时需要更新太多文档 | 增加一个版本号字段 快速过滤掉不需要升级的文档 升级时对不同版本的文档做不同处理 |
近似计算
统计网页点击流量时,每访问一个页面都会产生一次数据库计数更新操作,统计数字准确性并不十分重要
可以通过近似计算的方式减少写入数据库的次数
if random(0,9) == 0
increment by 10
场景 | 痛点 | 设计模式方案及优点 |
网页计数 各种结果不需要准确的排名 | 写入太频繁,消耗系统资源 | 间隔写入,每隔n次写入一次 大量减少写入需求 |
预聚合
业绩排名、电影观看排行等需要精确统计的数字无法使用近似计算
传统解决方案是通过聚合计算来得出结果,但是需要消耗较多的资源,且聚合计算需要较长的时间
要计算某个商品销量:
{
product: ”Bike",
sku: “abc123456”,
quantitiy: 20394,
daily_sales: 40,
weekly_sales: 302,
monthly_sales: 1419
}
可以使用预聚合,加几个字段,在每次更新时顺便一起更新这些字段
db.inventory.update({_id:123},
{$inc: {
quantity: -1,
daily_sales: 1,
weekly_sales: 1,
monthly_sales: 1,
}
}
)
场景 | 痛点 | 设计模式方案及优点 |
准确排名 | 统计计算耗时,计算时间长 | 模型中直接增加统计字段 每次更新数据时同时更新统计值 |