MongoDB 3.4 学习笔记 (三):非关系数据库的 Schema

1. 非关系数据库的 Schema

1.1. Schema 设计原则

数据库 Schema 设计是基于数据库特性、数据属性和应用系统选择最好的数据表示形式的过程。关系数据库中的范式即是 Schema 设计原则。

1.1.1. Schema 基础

1.1.1.1. 一对一

一对一的关系是描述两个实体之间的唯一关系。

模型:

// 用户文档:
{
  name: "Peter Wilkinson",
  age: 27
}
// 地址文档
{
  street: "100 some road",
  city: "Nevermore"
}
  • 内嵌方式

    将地址文档内嵌入用户文档,这样在检索用户信息和地址信息时,只需要一次读操作即可:

    {
      name: "Peter Wilkinson",
      age: 27,
      address: {
        street: "100 some road",
        city: "Nevermore"
      }
    }
    
  • 连接方式

    使用外键将两个文档连接,这也是关系数据库使用的方式,但是 MongoDB 没有外键的限制,只将外键作为 Schema 层面的一种关系:

    // 用户文档
    {
      _id: 1,
      name: "Peter Wilkinson",
      age: 27
    }
    
    // 地址文档
    {
      user_id: 1, // 用户外键
      street: "100 some road",
      city: "Nevermore"
    }
    

很明显在非关系数据库中,内嵌方式更有效。

1.1.1.2. 一对多

一对多的关系主要体现在其中一方的实体对应另一方多个实体,其中博客和评论尤为典型:

模型:

// 博客文档
{
  title: "An awesome blog",
  url: "http://awesomeblog.com",
  text: "This is an awesome blog we have just started"
}
// 评论文档
{
  name: "Peter Critic",
  created_on: ISODate("2014-01-01T10:01:22Z"),
  comment: "Awesome blog post"
}
{
  name: "John Page",
  created_on: ISODate("2014-01-01T11:01:22Z"),
  comment: "Not so awesome blog"
}
  • 内嵌方式

    将评论以数组形式内嵌入博客文档中:

    {
      title: "An awesome blog",
      url: "http://awesomeblog.com",
      text: "This is an awesome blog we have just started",
      comments: [{
        name: "Peter Critic",
        created_on: ISODate("2014-01-01T10:01:22Z"),
        comment: "Awesome blog post"
      }, {
        name: "John Page",
        created_on: ISODate("2014-01-01T11:01:22Z"),
        comment: "Not so awesome blog"
      }]
    }
    

    这种方式需要注意三点:

    • 文档的大小的上限是 16M 。
    • 在添加评论时会将文档复制到内存中的新位置并更新所有的索引,这会严重影响写性能。
    • 每次检索都会将所有的评论返回,无法实现分页功能。
  • 连接方式

    与一对一关系相似,都是利用外键实现连接:

    // 博客文档
    {
      _id: 1,
      title: "An awesome blog",
      url: "http://awesomeblog.com",
      text: "This is an awesome blog we have just started"
    }
    
    {
      blog_entry_id: 1,
      name: "Peter Critic",
      created_on: ISODate("2014-01-01T10:01:22Z"),
      comment: "Awesome blog post"
    }
    {
      blog_entry_id: 1,
      name: "John Page",
      created_on: ISODate("2014-01-01T11:01:22Z"),
      comment: "Not so awesome blog"
    }
    

    连接方式在评论数不多时效果很好,但当评论数很多时会影响读操作的性能。

  • 桶装方式

    桶装方式是对前两种方式的混合,尝试使用连接方式的灵活性来解决嵌入方式笨重,同时平衡读操作的性能。方法就是将评论按一定数量灌入的桶中,每一个桶都是用连接方式和博客关联。

    // 博客文档
    {
      _id: 1,
      title: "An awesome blog",
      url: "http://awesomeblog.com",
      text: "This is an awesome blog we have just started"
    }
    
    {
      blog_entry_id: 1,
      page: 1,
      count: 50,
      comments: [{
        name: "Peter Critic",
        created_on: ISODate("2014-01-01T10:01:22Z"),
        comment: "Awesome blog post"
      }, ...]
    }
    {
      blog_entry_id: 1,
      page: 2,
      count: 1,
      comments: [{
        name: "John Page",
        created_on: ISODate("2014-01-01T11:01:22Z"),
        comment: "Not so awesome blog"
      }]
    }
    

    桶装方式非常适合利用时间或编号进行分页操作的文档。

1.1.1.3. 多对多

双方的实体间都对应了多个关系即为多对多关系。典型代表是图书与作者:

  • 双向内嵌

    作者文档中包含图书的外键,图书文档中也包含作者的外键。

    // 作者文档
    {
      _id: 1,
      name: "Peter Standford",
      books: [1, 2]
    }
    {
      _id: 2,
      name: "Georg Peterson",
      books: [2]
    }
    
    // 图书文档
    {
      _id: 1,
      title: "A tale of two people",
      categories: ["drama"],
      authors: [1]
    }
    {
      _id: 2,
      title: "A tale of two space ships",
      categories: ["scifi"],
      authors: [1, 2]
    }
    

    解耦多对多关系:

    如果是图书和图书类别的多对多关系,可以想象的到图书类别的文档中将包含成千上万的图书外键。所以需要另外一种方式来实现多对多关系.

  • 单向内嵌

    当一方的关系明显多余另外一方,此时就应该考虑使用单向内嵌的方式来实现多对多的关系。

    // 图书类别文档
    {
      _id: 1,
      name: "drama"
    }
    
    // 图书文档
    {
      _id: 1,
      title: "A tale of two people",
      categories: [1],
      authors: [1, 2]
    }
    

1.1.2. 实例

在非关系数据库中没有固定的 Schema 设计原则,它是弹性的可根据多种因素灵活选择。下面通过商品实例分析非关系数据库的 Schema 设计思想:

{
  _id: ObjectId("4c4b1476238d3b4dd5003981"), 
  slug: "wheelbarrow-9092",
  sku: "9092",
  name: "Extra Large Wheelbarrow",
  description: "Heavy duty wheelbarrow...",
  details: {
    weight: 47,
    weight_units: "lbs",
    model_num: 4039283402,
    manufacturer: "Acme",
    color: "Green"
  },
  total_reviews: 4,
  average_review: 4.5,
  pricing: {
    retail: 589700,
    sale: 489700,
  },
  price_history: [
    {
      retail: 529700,
      sale: 429700,
      start: new Date(2010, 4, 1),
      end: new Date(2010, 4, 8)
    },
    {
      retail: 529700,
      sale: 529700,
      start: new Date(2010, 4, 9),
      end: new Date(2010, 4, 16)
    },
  ],
  primary_category: ObjectId("6a5b1476238d3b4dd5000048"),
  category_ids: [
    ObjectId("6a5b1476238d3b4dd5000048"),
    ObjectId("6a5b1476238d3b4dd5000049")
  ],
  main_cat_id: ObjectId("6a5b1476238d3b4dd5000048"),
  tags: ["tools", "gardening", "soil"],
}
  • 内嵌文档(一对一关系)

    商品实例中有个键 details 指向一个子文档,这样的好处是既可以灵活的规划商品的属性,又为程序员对文档的查询提供了模块化的操作。与此类似的有 pricing 键和 price_history 键,而 price_history 键指向的是一个数组。

  • 一对多关系

    商品只属于一个类别,而类别可以拥有多种商品。 primary_category 键对应的是一个类别的 ID,表明该商品属性哪一个类别。

  • 多对多关系

    应用可能需要列出与该商品相关的类别。这种操作在关系数据库中通常建立一个多对多的表,然后通过 join 方法连接多个表。而在非关系数据库中,只需要像 category_ids 键这样指向与该商品相关的类别 ID 数组即可。在 mongoose 中提供了 population 语法糖实现了用文档来填充 ID 。

  • 优化与去范式

    每个商品会有多个评价:

      {
      _id: ObjectId("4c4b1476238d3b4dd5000041"),
      product_id: ObjectId("4c4b1476238d3b4dd5003981"),
      date: new Date(2010, 5, 7),
      title: "Amazing",
      text: "Has a squeaky wheel, but still a darn good wheelbarrow.",
      rating: 4,
      user_id: ObjectId("4c4b1476238d3b4dd5000042"),
      username: "dgreenthumb",
      helpful_votes: 3,
      voter_ids: [ 
        ObjectId("4c4b1476238d3b4dd5000033"),
        ObjectId("7a4f0376238d3b4dd5000003"),
        ObjectId("92c21476238d3b4dd5000032")
      ]
    }
    

    可以看出 user_id 键是用来关联评价与用户,但是因为 MongoDB 不支持 join 连接,而评价的内容中需要展示用户名,为此添加 username 键,也就是选择优化减少成本,而不去选择去范式。相似的还有 MongoDB 中不允许查询文档中数组的大小, 添加 helpful_votes 键可以轻松的实现这一功能。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容