引言
在数据密集型应用设计中,数据模型处于核心地位,描述了数据以怎样的方式被组织在一起,对应用的设计方案有着深远影响。
大部分应用程序都是分层的,每一层提供一套数据模型抽象,屏蔽下一层的复杂细节,围绕这一层抽象,各层之间的开发人员能够高效地沟通合作。
现实中存在着各种各样的数据模型,每一种模型都包含了对其使用场景的假设,在某些场景,该数据模型使用简单且运行高效,但是在某些场景,该数据模型配置复杂甚至无法支持。深入理解数据模型使用场景的相关假设,对于应用程序的技术选型和最终实现至关重要。
本章主要描述各种通用的数据模型以及相应的数据查询语言。
关系型数据模型
大家最熟悉的数据模型,要数关系型数据模型(relational model),对应的查询语言SQL,数据应用开发人员几乎每天都要使用。
在关系型数据模型中,数据以关系的形式被组织起来(relation,对应的是数据库中的表), 每一种关系,包含了一组元组的集合(tuple, 对应的是数据表中的行)。
关系型数据模型,存储在关系型数据库中,通过SQL进行查询和更新。今天的互联网应用,比如搜索引擎、社交网络、电子商务网站等,关系型数据库是在背后支撑其运行的核心组件。
声明式查询语言
关系型数据模型的一个重要贡献,就是引入了数据查询语言SQL。
历史上,关系型数据模型的那些已经失败的竞争对手,比如网络模型(network model)和层级模型(hierarchical),采用的是命令式(imperative)查询语言来获取数据。
举个例子,如果要从一组动物数据局中查出所有的鲨鱼信息,命令式语言的伪代码:
1 | function getSharks() { |
命令式查询的限制在于,程序必须明确地告诉数据系统具体的执行步骤,以便系统能够一步步执行完成任务。这就要求程序员了解数据库系统的实现细节和约定,造成了程序和数据库系统的紧耦合。
而对于关系型数据库来说,使用的SQL是一种声明式(declarative)语言,以鲨鱼信息查询为例,SQL写成:
1 | SELECT * FROM animals WHERE family = 'Sharks'; |
程序只需要声明想获取的数据,而由数据库系统自己决定改如何获取信息,查询引擎可以基于不同的SQL,进行解析优化、索引优化,大大提高了查询的速度。
结构失配
结构失配是关系型数据模型主要缺陷之一。
今天大部分应用是以面向对象的模式设计的,一个对象中可以包含各种类型的数据:字符串、数组、字典、其它对象等等,对象通常是以树形结构在内存中存储的。
但是在关系型数据库中,数据是按行列模式存储的,是一个二维结构。于是内存中的数据和数据库中的数据,就存在着一个结构上的失配(mismatch):应用程序在向数据库中存储对象之前,往往要写一层翻译的代码,将树形结构转化为二维结构。
虽然通过使用ORM框架(比如ActiveRecord、Hibernate),可以减少这部分翻译工作所需的代码量,但是结构失配的问题仍然存在。
文档型数据模型
针对关系型数据库中存在的缺陷,2010年之后,出现了NoSQL的风潮。NoSQL是一个容易引起误解的名词,它不是指非SQL,而是指不仅仅是SQL(Not Only SQL),NoSQL蓬勃发展的背后驱动力包括:
- 传统关系数数据库通常难以扩展。在云计算大数据时代,需要使用更易扩展的方案,来处理海量数据的存取。
- 传统关系型数据库本身存在着诸多限制,比如处理某些特定的查询类型时性能不好,关系模型不够灵活等等。
文档型数据模型,典型的实现如MongoDB,以文档的形式存储数据,不再受行列二维结构的约束,克服了传统关系型模型的限制。在正确使用的场景,可以获得以下优势:
更简单的代码
如果应用中的对象都是树形结构,那么使用文档数据模型,没有结构失配的问题,对应的代码更加简单。
传统的文档数据模型支持数据间join操作比较困难,这在实际应用场景中可能是一个问题。
更灵活的存储结构
和关系型数据模型不同,文档数据模型通常对数据存储结构没有强制约束。
文档型数据库经常被称作是无结构(schemaless)的,实际上不太准确,应用程序在读取数据的时候,隐含着数据结构的假设,所以更准确的说法是读时结构约束(schema-on-read),对应的关系型数据库,则是在写入数据时需要遵循显式的结构定义,称作写时结构约束(schme-on-read)。这两种约束之间,有点像动态编程语言(运行时类型检查)和静态编程语言(编译时类型检查)的区别。
良好的查询性能
在文档数据模型,数据通常是以文档的粒度被读取的,整个文档被一次性全部加载到内存中,后续对文档内容的查询可以全部在内存中完成,所以具有良好的查询性能。
互相借鉴
关系型数据模型和文档型数据模型,彼此之间并非泾渭分明,它们在各自发展的过程中也在互相借鉴。
现代的关系型数据库,比如MySQL在5.7版本之后,也支持json格式的字段类型,增加了结构的灵活度。对于MongoDB来说,也支持简单join操作,突破传统的文档型数据库在数据关联方面的限制。
图数据模型
如果我们的场景中,主要存在的是一对多关系,即数据间是树形结构,那么使用文档数据模型是合适的,但是如果存在着大量的多对多关系,即数据间是图结构,那么文档模型就不能够很好描述这些关系了,此时更适合采用图数据模型。
图数据结构,由顶点(vertices)和边(edges)组成,很多场景的数据可以用图来描述,比如:
- 社交网络。顶点代表用户,边代表用户互相认识。
- web页面。顶点代表网页,边代表跳转到其它网页的链接。
- 地图。顶点代表地点,边代表道路。
用一个场景作为示例:一对夫妻Lucy和Alain,Lucy来自Idaho,Alain来自Beaune,现在他们住在London。

针对这个场景,我们可以用属性图(Property Graphs)来描述数据之间的多对多关系。
属性图
属性图的每个点包含:
- 一个唯一id
- 一组出边(outgoing edges)的集合
- 一组入边(incoming edges)的集合
- 一组属性的集合
每条边包含:
- 一个唯一id
- 边的起点
- 边的终点
- 描述关系类型的标签
- 一组属性的集合
为了便于理解,可以想象成由两张关系型数据表来存储图模型,一张表存储节点信息,一张表存储边信息:
1 | CREATE TABLE vertices ( |
需要注意的是:
- 每个节点可以通过一条边连接到另外一个节点,没有任何的存储结构限制。
- 对于任何一个节点,可以很容易的查出它的所有入边(incoming edges)和出边(outgoing edges),也就是说可以很容易地遍历整个图。
- 通过使用不同的标签(label),可在一张图里面,存储各种不同类型的信息。
总结
不同的场景有不同的需求,没有一个放之四海而皆准的解决方案,彼之蜜糖也许是吾之砒霜。
在可以预见的未来,关系型数据模型仍将处于主导地位,并且和文档数据模型、图数据模型等多种非关系型数据模型并存。