MongoDB数据模型

MongoDB数据模型

杰子学编程 32 2022-06-01

MongoDB 三 数据模型

1、什么是数据模型

数据模型是一组符合、文本组成的集合,用以准确表达信息,达到有效交流、沟通的目的。

比如:保险公司经济人,我们关注的客户主要关注:客户年龄,年收入,家庭成员,工作行业等等

而作为理发店,则主要关注每个月来几次,对发型的需求等等。关注的属性和保险公司所关注的属性完全不同。

2、数据模型设计的元素

2.1、实体Entity

  • 描述业务的主要数据集合

  • 谁,什么,何时,何地,为何,如何。

    entity

2.2、属性 Attribute

属性是进一步用来描述实体的,比如我们看下联系人这个实体,它包含了姓名、公司、职称等等信息。

2.3、关系 Relationship

关系用来描述实体之间的关系,比如一个人包含多个地址,可能家庭住址,公司住址等等。

  • 结构规则:1-N;1-1;N-N;

3、传统模型设计:从概念到逻辑到物理

模型

  • 概念模型
    • 往往是由用户和需求分析来进行讨论,只是粗旷的了解下大体业务逻辑,梳理业务需求
  • 逻辑模型
    • 基于业务需求,由架构师或者需求分析师,进行实体及实体属性和他们之间的管理罗列出来。
  • 物理模型
    • 具体实现,通过使用什么数据库,将逻辑模型建立起来。

4、开发者角度下的物理模型

物理模型,主流使用的是关系型模型,主要遵循三范式原则。

model

遵循三范式的优点:可以减少数据冗余,数据表体积小更新快,范式化的更新操作比 反范式化更快,范式化的表通常比反范式化更小。
缺点:对于查询需要对多个表,会关联多个表,在应用中,进行表关联的成本是很高
更难得进行索引优化
反范式化设计的优缺点
可以减少表的关联,可以对查询更好的进行索引优化,缺点,表结构存在数据冗余和数据维护异常,对数据的修改需要更多资源。
因此在设计数据库结构的时候要将反范式化和范式化结合起来。

二、JSON文档模型设计

2.1、MongoDB文档模型设计的三个误区

  • 不需要模型设计
  • MongoDB应该用一个超级大文档来组织所有数据
  • MongoDB不支持关联或事物

以上这些说法全是错误的。

2.2、关于JOSN文档模型设计

文档模型设计处于是物理模型设计阶段 (PDM)

JSON 文档模型通过内嵌数组或引用字段来表示关系

文档模型设计不遵从第三范式,允许冗余。

严格来说,MongoDB 同样需要概念/逻辑建模

文档模型设计的物理层结构可以和逻辑层类似

2.3、逻辑模型 – JSON 模型

下面是一个JSON模型和逻辑模型的对比, 这里json文档中的联系人,列举出了姓名、性别、创建日期、所属组、和地址;

这样一个物理关系模型中需要多张表,而这里可以直接通过一个json来表述出来。

2.4、文档模式设计原则:性能和易用

在文档模式设计中我们是没有第三范式原则的,这是个好处也是个弊端,很多人不知道该如何设计这个模型。什么样的模型好什么样的模型坏。

我们是有两个关键点,看你这个模型是否性能不错,能够支撑高并发、低延迟的读写。另外一个角度在程序开发过程中是否简易。

这里没有唯一的标准,这里更多的是经验之谈。我们后面给大家些建模的建议。可以尊从文档模式设计三步走:

screenshot-20220409-183832

三、文档模式设计:基础建模

  • 根据概念模型或者业务需求推导出逻辑模型 – 找到对象

  • 列出实体之间的关系(及基数) - 明确关系

  • 套用逻辑设计原则来决定内嵌方式 – 进行建模

  • 完成基础模型构建

3.1、1:1关系建立

比如,一个联系人只有一个头像,

  • 基本原则:一对一关系以内嵌为主作为子文档形式 或者直接在顶级不涉及到数据冗余;
  • 例外情况:如果内嵌后导致文档大小超过16MB
{
  "name": "TJ Tang",
  "company": "TAPDATA",
  "title": " CTO",
  "portraits": {
    "mimetype": "xxx",
    "data": "xxxx"
  }
}

3.2、一对多关系建立

比如一个联系人可以有多个地址:

  • 一对多关系同样以内嵌为主用数组来表示一对多不涉及到数据冗余;
  • 例外情况:如果内嵌后导致文档大小超过16MB,数组长度太大(数万或更多),数组长度不确定;
{
  "name": "TJ Tang",
  "company": "TAPDATA",
  "title": " CTO",
  "portraits": {
    "mimetype": "xxx",
    "data": "xxxx"
  },
  "addresses": [
    {
      "type": "home",
      ....
    },
    {
      "type": "work",
      ....
    }
  ]
}

3.3、多对多关系建模

比如一个联系人可以有多个地址:

  • 一对多关系同样以内嵌为主用数组来表示一对多,通过冗余来实现N-N;
  • 例外情况:如果内嵌后导致文档大小超过16MB,数组长度太大(数万或更多),数组长度不确定;
{
  "name": "TJ Tang",
  "company": "TAPDATA",
  "title": " CTO",
  "portraits": {
    "mimetype": "xxx",
    "data": "xxxx"
  },
  "addresses": [
    {
      "type": "home",
      ....
    },
    {
      "type": "work",
      ....
    }
  ],
  "groups":[
    {"name":"FRI"},
    {"name":"DF"},
  ]
}

四、文档模式设计:工况细化

在做工况细化过程中,我们需要根据技术需求对我们模型进行调整,这是个技术导向,我们需要和业务方进行详细的沟通,了解到数据的使用方法,是根据什么来查询的。如:单个查询,报表查询,查询参数是什么、数量有多大、读写比例有多少等等。针对不同需求,我们可以引入引用、关联、或者冗余等手段来解决这些问题。

● 最频繁的数据查询模式

● 最常用的查询参数

● 最频繁的数据写入模式

● 读写操作的比例

● 数据量的大小

比如:联系人管理应用的分组需求

screenshot-20220409-185810

4.1 解决方案:Group 使用单独的集合

类似于关系型设计,使用 id 或者唯一键关联,使用 $lookup 来提供一次查询多表的能力(类似关联)(3.2以上版本支持)。

lookup

4.2、联系人的头像: 引用模式

  • 头像使用高保真,大小在 5MB-10MB
  • 头像一旦上传,一个月不可更换
  • 基础信息查询(不含头像)和 头像查询的比例为 9 :1

建议: 使用引用方式,把头像数据放到另外一个集合,可以显著提升 90% 的查询效率。

screenshot-20220409-190552

4.3、什么时候使用引用模式

  • 内嵌文档太大,数 MB 或者超过 16MB
  • 内嵌文档或数组元素会频繁修改
  • 内嵌数组元素会持续增长并且没有封顶

MongoDB 引用设计的限制

  • MongoDB 对使用引用的集合之间并无主外键检查
  • MongoDB 使用聚合框架的 $lookup 来模仿关联查询
  • $lookup 只支持 left outer join
  • $lookup 的关联目标(from)不能是分片表

五、文档设计模式:套用设计模式

文档模型:无范式,无思维定式,充分发挥想象力;

设计模式:实战过屡试不爽的设计技巧,快速应用;

举例:一个IoT场景的分桶设计模式,可以帮助把存储空间降低10倍并且查询效率提升数十倍。

问题:物联网场景下的海量数据处理 - 设备监控数据

上报数据格式如下:

{
  "_id":"10000000022200000222:CA2091",
  "icao":"CA2091",
  "ts":ISODate("2022-04-03T20:21:35.000+0000"),
  "events":{
    "tem":35.3,
    "humidity":50.3,
    "lon":38.345,
    "lat":58.987,
    "open":"0",
    "b":"b"
    "p":[123,245],
    "s":91
  }
}

我们假设有10万个设备,每分钟上报一条数据,一年的数据量大约52560*100W = 525亿数据,约10TB;

每分钟1条 计算说明
文档条数 52.5B 10W * 365 * 24 * 60
索引大小 6364GB 10W * 365 * 24 * 60 * 130
_id index 1468GB
4895GB
文档平均大小 92Bytes
数据大小 4503GB 10W * 365 * 24 * 60 * 92

5.2、解决方案-分桶设计

思路将之前每分钟数据汇总到每小时一条,将每分钟数据存放在events中,变更结构如下:

{
  "_id": "10000000022200000222:CA2091",
  "icao": "CA2091",
  "ts": ISODate("2022-04-03T20:00:00.000+0000"),//每小时
  "events": [//每小时60条数据,集合条数固定
    {
      "tem": 35.3,
      "humidity": 50.3,
      "lon": 38.345,
      "lat": 58.987,
      "open": "0",
      "b": "b",
      "p": [
        123,
        245
      ],
      "s": 91,
      "ts": ISODate("2022-04-03T20:01:00.000+0000")//每分钟
    },
    {
      "tem": 35.3,
      "humidity": 50.3,
      "lon": 38.345,
      "lat": 58.987,
      "open": "0",
      "b": "b",
      "p": [
        123,
        245
      ],
      "s": 91,
      "ts": ISODate("2022-04-03T20:02:00.000+0000")
    }
  ]
}

这里我们一个文档可以存储一个小时的设备数据,而events集合中的数据条数是固定的,每小时60条。

可视化表现 24 小时的设备数据,查询1440 次读操作即可。

每分钟1条 每小时一个文档
文档条数 52.5B 876 M
索引大小 6364GB 106 GB
_id index 1468GB 24.5 GB
4895GB 81.6 GB
文档平均大小 92Bytes 758 Bytes
数据大小 4503GB 618 GB

分桶方式总结:

场景 痛点 设计模式方案及优点
时序数据 数据点采集频繁,数据量太多 利用文档内嵌数组,将一个时间段的数据聚合到一个文档里
物联网 大量减少文档数量
智慧城市 大量减少索引占用空间
智慧交通

六、设计模式集锦

6.1、列转行模式

问题: 大文档,很多字段,很多索引

screenshot-20220409-215935

解决方案:列转行

screenshot-20220409-220021

  • 列转行总结:
场景 痛点 设计模式方案及优点
产品属性 ‘color’, ‘size’, ‘dimensions’, …物联网,多语言(多国家)属性 文档中有很多类似的字段,会用于组合查询搜索,需要建很多索引 转化为数组,一个索引解决所有查询问题

6.2、版本字段

问题:模型灵活了,如何管理文档不同版本?

修改前版本:

{
  "_id": ObjectId("5de26f197edd62c5d388babb"),
  "name": "TJ",
  "company": "Tapdata"
}

新增手机号后版本:

{
  "_id": ObjectId("5de26f197edd62c5d388babb"),
  "name": "TJ",
  "company": "Tapdata",
  "phone":"182XXXX8888"
}

解决方案:增加一个版本字段:

{
  "_id": ObjectId("5de26f197edd62c5d388babb"),
  "name": "TJ",
  "company": "Tapdata",
  "phone":"182XXXX8888",
  "schema_version": 2.0
}
  • 版本字段总结:
场景 痛点 设计模式方案及优点
任何有版本衍变的数据库 文档模型格式多,无法知道其合理性,升级时候需要更新太多文档 增加一个版本号字段;快速过滤掉不需要升级的文档;升级时候对不同版本的文档做不同的处理

6.3、近似计算

问题:统计网页点击流量

screenshot-20220409-221016

  • 解决方案: 用近似计算

每隔10 (X)次写一次,

  • 近似计算总结:
场景 痛点 设计模式方案及优点
网页计数;各种结果不需要准确的排名; 写入太频繁,消耗系统资源 间隔写入,每隔10次或者100次,大量减少写入需求

6.4、使用预聚合字段

问题: 业绩排名,游戏排名,商品统计等精确统计

热销榜:某个商品今天卖了多少,这个星期卖了多少,这个月卖了多少?

电影排行:观影者,场次统计

传统解决方案:通过聚合计算

痛点:消耗资源多,聚合计算时间长

  • 解决方案: 用预聚合字段

在模型中新增预聚合字段,每次更新数据的时候同步更新统计值。

{
  "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,
	} 
})
  • 预聚合字段总结:
场景 痛点 设计模式方案及优点
准确排名,排行榜 统计计算耗时,计算时间长 模型中直接增加统计字段;每次更新数据时候同时更新统计值

注:以上内容参考《MongoDB 高手课》

更多内容请求关注公众号:杰子学编程


# Java # SpringBoot # MongoDB