大多数应用程序是通过一层一层叠加数据模型来构建的。每一层都面临的关键问题是:如何将其用下一层来表示?例如:

  1. 作为一名应用程序开发人员,观测现实世界(其中包括人员、组织、货物、行为、资金流动、传感器等),通过对象或数据结构,以及操作这些数据结构的 API 来对其建模。这些数据结构往往特定于该应用。
  2. 当需要存储这些数据结构时,可以采用通用数据模型(例如 JSON 或 XML 文档、关系数据库中的表或图模型)来表示。
  3. 数据库工程师接着决定用何种内存、磁盘或网络的字节格式来表示上述 JSON/XML/关系/图形数据。数据表示需要支持多种方式的查询、搜索、操作和处理数据。
  4. 在更下一层,硬件工程师则需要考虑用电流、光脉冲、磁场等来表示字节。

复杂的应用程序可能会有更多的中间层,但是基本思想相同:每层都通过提供一个简洁的数据模型来隐藏下层的复杂性。有许多不同类型的数据模型,每种数据模型都有其最佳使用的若干假设。

关系模型与文档模型

现在最著名的数据模型可能是 SQL,它基于 1970 年提出的关系模型:数据被组织成关系,在 SQL 中称为表,其中每个关系都是元组的无序集合(在 SQL 中称为行)

NoSQL

进入 21 世纪,NoSQL 成为推翻关系模式主导地位的一个强力竞争者。采用 NoSQL 数据库有这样几个驱动因素,包括:

  • 比关系数据库更好的扩展性需求,包括支持超大数据集或超高写入吞吐量。
  • NoSQL 普遍是免费和开源软件。
  • 关系模型不能很好地支持一些特定的查询操作
  • 关系模型具有一些限制性,NoSQL 的数据模型更具动态性和表达力。

对象—关系不匹配

现在大多数应用开发都采用面向对象的编程语言,由于兼容性问题,普遍对SQL数据模型存在抱怨:如果数据存储在关系表中,那么应用层代码中的对象与表、行和列的数据库模型之间需要一个笨拙的转换层。

例如,如何在关系模式中表示简历。整个简历可以通过唯一的标识符 user_id 来标识。像 first_name 和 last_name 这样的字段在每个用户中只出现一次,所以可以将其建模为 users 表中的列。然而,大多数人在他们的职业中有一个以上的工作,并且可能有多个教育阶段和任意数量的联系信息。用户与这些项目之间存在一对多的关系,可以用多种方式来表示:

  • 在传统的 SQL 模型中,最常见的规范化表示是将职位、教育和联系信息放在单独的表中,并使用外键引用 users 表,如下图所示。
  • 之后的 SQL 标准增加了对结构化数据类型和 XML 数据的支持。这允许将多值数据存储在单行内,并支持在这些文档中查询和索引
  • 第三个选项是将工作、教育和联系信息编码为 JSON 或 XML 文档,将其存储在数据库的文本列中,并由应用程序解释其结构和内容。对于此方法,通常不能使用数据库查询该编码列中的值。

image.png

对于像简历这样的数据结构,它主要是一个自包含的文档,因此用 JSON 表示非常合适,参见如下示例。面向文档的数据库(如 MongoDB、RethinkDB、CouchDB 和 Espresso)都支持该数据模型。

{
    "user_id":       251,
    "first_name" :   "Bill",
    "last_name" :    "Gates",
    "summary":       "Co-chair of the Bill & Melinda Gates... Active blogger.",
    "region_id" :    "us:91",
    "industry_id" :  131,
    "photo_url":     "/p/7/000/253/05b/308dd6e.jpg",
    "positions" : [
        {"job_title":"Co-chair", "organization":"Bill 8 Melinda Gates Foundation"},
        {"job_title":"Co-founder,Chairman", "organization" : "Microsoft"}
    ],
    "education":[
        {"school_name":"Harvard University","start" : 1973,"end": 1975},
        {"school_name":"Lakeside School,Seattle", "start": null,"end": null}
    ],
    "contact_info": {
        "blog":    "http: /lthegatesnotes.com",
        "twitter" :"http: //twitter.com/BillGates"
    }
}

JSON 表示比多表模式具有更好的局部性。如果要在关系模式中读取一份简历,那么要么执行多个查询,要么在 users 表及其从属表之间执行混乱的多路联结。而对于 JSON 表示方法,所有的相关信息都在一个地方,一次查询就够了。

多对一与多对多的关系

在上面的示例中,region_id 和 industry_id 定义为 ID,而不是纯文本字符串形式,为什么这样做呢?

如果用户界面是可以输入地区或行业的自由文本字段,则将其存储为纯文本字符串更有意义。但是,拥有地理区域和行业的标准化列表,并让用户从下拉列表或自动填充器中进行选择会更有优势,原因有:

  • 所有的简历保持样式和输入值一致
  • 避免歧义(例如,如果存在一些同名的城市)。
  • 易于更新:名字只保存一次,如果需要改变,可以很容易全面更新。
  • 本地化支持:当网站被翻译成其他语言时,标准化的列表可以方便本地化。
  • 更好的搜索支持:例如,搜索华盛顿州的慈善家可以匹配到这个简历,这是因为地区列表可以将西雅图属于华盛顿的信息编码进来。

使用 ID 的好处是,因为它对人类没有任何直接意义,所以永远不需要直接改变:即使 ID 标识的信息发生了变化,它也可以保持不变。任何对人类有意义的东西都可能在将来某个时刻发生变更。如果这些信息被复制,那么所有的冗余副本也都需要更新。这会导致更多写入开销,并且存在数据不一致的风险。消除这种重复正是数据库规范化的核心思想。

然而这种数据规范化需要表达多对一的关系,这并不是很适合文档模型。文档模型是某种方式的层次模型:即在其父记录中保存了嵌套记录,而不是存储在单独的表中。

但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项都由唯一的标识符引用,该标识符在关系模型中被称为外键,在文档模型中被称为文档引用。标识符可以查询时通过联结操作或相关后续查询来解析。

对于关系数据库,由于支持联结操作,可以很方便地通过ID来引用其他表中的行。而在文档数据库中,一对多的树状结构不需要联结,支持联结通常也很弱。

即使应用程序的初始版本非常适合采用无联结的文档模型,但随着应用支持越来越多的功能,数据也变得更加互联一体化。

两种模型的比较

在比较关系数据库与文档数据库时,需要考虑很多方面的差异,包括它们的容错性和并发处理,这里只关注数据模型中的差异。

支持文档数据模型的主要论点是模式灵活性,由于局部性而带来较好的性能,对于某些应用来说,它更接近于应用程序所使用的数据结构。关系模型则强在联结操作、多对一和多对多关系更简洁的表达上,与文档模型抗衡。

哪种模型的应用代码更简单?

如果应用数据具有类似文档的结构(即一对多关系树,通常一次加载整个树),那么使用文档模型更为合适。而关系型模型则倾向于某种数据分解,它把文档结构分解为多个表,有可能使得模式更为笨重,以及不必要的应用代码复杂化。

文档模型也有一定的局限性:例如,不能直接引用文档中的嵌套项,而需要说“用户 251 的职位列表中的第二项”。然而,只要文档嵌套不太深,这通常不是问题。

在文档数据库中,对联结的支持不足是否是问题取决于应用程序。例如,在使用文档数据库记录事件发生时间的应用分析程序中,可能永远不需要多对多关系。

但是,如果应用程序确实使用了多对多关系,那么文档模型就变得没什么吸引力。虽然可以通过反规范化来减少对联结的需求,但是应用程序代码需要做额外的工作来保持非规范化数据的一致性。通过向数据库发出多个请求,可以在应用程序代码中模拟联结,但是这也将应用程序变得复杂,并且通常比数据库内的专用代码执行的联结慢。在这些情况下,使用文档模型会导致应用程序代码更复杂、性能更差。

通常无法一概而论哪种数据模型的应用代码更简单。这主要取决于数据项之间的关系类型。对于高度关联的数据,文档模型不太适合,关系模型可以胜任,而图模型则是最为自然的。

文档模型的灵活性

大多数文档数据库,以及关系数据库中的 JSON 支持,都不会对文档中的数据强制执行任何模式验证。关系数据库中的 XML 通常支持带有可选的模式验证功能。没有模式验证意味着可以将任意的键-值添加到文档中,并且在读取时,客户端无法保证文档可能包含哪些字段。

文档数据库是读时模式(数据的结构是隐式的,只有在读取时才解释),与写时模式(关系数据库的一种传统方法,模式是显式的,并且数据库确保数据写入时都必须遵循)相对应。读时模式类似编程语言中的动态(运行时)类型检查,而写时模式类似于静态(编译时)类型检查。

当应用程序需要改变数据格式时,这些方法之间的差异就变得尤其明显。例如,当前用户的全名存储在一个字段中,而现在想分别存储名字和姓氏。在文档数据库中,只需使用新字段来编写新文档,并在应用层来处理读取旧文档的情况。而在关系数据库中,则需要先更改表结构,然后运行 UPDATE 语句进行全量更新。

注:在大表上运行 UPDATE 语句,对于任何关系数据库都可能会很慢,因为每一行都需要重写。应用程序可以将 first_name 设置为默认值 NULL,并在读取时填充它。

如果集合中的项由于某种原因,并不都具有相同的结构,例如:

  • 有许多不同类型的对象,将每种类型的对象都保存在各自的表中不太现实。
  • 数据的结构由无法控制的外部系统所决定,而且可能随时改变。

在这些情况下,模式带来的损害大于它所能提供的帮助,无模式文档可能是更自然的数据模型。但是,当所有记录都有相同结构时,模式则是记录和确保这种结构的有效机制。

查询的数据局部性

文档通常存储为 JSON、XML 或其二进制变体,如果应用程序需要频繁访问整个文档,则存储局部性具有性能优势。如果数据被划分在多个表中,则需要进行多次索引查找来检索所有数据,中间可能需要更多的磁盘 I/O 并花费更多的时间。

局部性优势仅适用需要同时访问文档大部分内容的场景。由于数据库通常会加载整个文档,如果应用只是访问其中的一小部分,则对于大型文档数据来讲就有些浪费。对文档进行更新时,通常会重写整个文档,而只有修改量不改变源文档大小时,原地覆盖更新才更有效。因此,通常建议文档应该尽量小且避免写入时增加文档大小。这些性能方面的不利因素大大限制了文档数据库的适用场景。

文档数据库与关系数据库的融合

随着时间的推移,关系数据库与文档数据库变得越来越相近,或许这是一件好事:数据模型可以相互补充。如果数据库能够很好处理文档类数据,还能对其执行关系查询,那么应用程序可以使用最符合其需求的功能的组合。

数据查询语言

SQL 是一种声明式查询语言,而很多常用的编程语言则是命令式。这两种之间有什么差别?

命令式语言告诉计算机以特定顺序执行某些操作;而对于声明式查询语言,则只需指定所需的数据模式,结果需满足什么条件,以及如何转换数据,而不需指明如何实现这一目标。数据库系统的查询优化器会决定采用哪些索引和联结,以及用何种顺序来执行查询的各个语句。


与昊
222 声望634 粉丝

IT民工,主要从事web方向,喜欢研究技术和投资之道