设计数据密集型应用2 —— 数据模型与查询语言

引言

在数据密集型应用设计中,数据模型处于核心地位,描述了数据以怎样的方式被组织在一起,对应用的设计方案有着深远影响。

大部分应用程序都是分层的,每一层提供一套数据模型抽象,屏蔽下一层的复杂细节,围绕这一层抽象,各层之间的开发人员能够高效地沟通合作。

现实中存在着各种各样的数据模型,每一种模型都包含了对其使用场景的假设,在某些场景,该数据模型使用简单且运行高效,但是在某些场景,该数据模型配置复杂甚至无法支持。深入理解数据模型使用场景的相关假设,对于应用程序的技术选型和最终实现至关重要。

本章主要描述各种通用的数据模型以及相应的数据查询语言。

关系型数据模型

大家最熟悉的数据模型,要数关系型数据模型(relational model),对应的查询语言SQL,数据应用开发人员几乎每天都要使用。

在关系型数据模型中,数据以关系的形式被组织起来(relation,对应的是数据库中的表), 每一种关系,包含了一组元组的集合(tuple, 对应的是数据表中的行)。

关系型数据模型,存储在关系型数据库中,通过SQL进行查询和更新。今天的互联网应用,比如搜索引擎、社交网络、电子商务网站等,关系型数据库是在背后支撑其运行的核心组件。

声明式查询语言

关系型数据模型的一个重要贡献,就是引入了数据查询语言SQL。

历史上,关系型数据模型的那些已经失败的竞争对手,比如网络模型(network model)和层级模型(hierarchical),采用的是命令式(imperative)查询语言来获取数据。

举个例子,如果要从一组动物数据局中查出所有的鲨鱼信息,命令式语言的伪代码:

1
2
3
4
5
6
7
8
9
function getSharks() {
var sharks = [];
for (var i = 0; i < animals.length; i++) {
if (animals[i].family === "Sharks") {
sharks.push(animals[i]);
}
}
return sharks;
}

命令式查询的限制在于,程序必须明确地告诉数据系统具体的执行步骤,以便系统能够一步步执行完成任务。这就要求程序员了解数据库系统的实现细节和约定,造成了程序和数据库系统的紧耦合。

而对于关系型数据库来说,使用的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
2
3
4
5
6
7
8
9
10
11
CREATE TABLE vertices (
vertex_id integerPRIMARYKEY,
properties json
);
CREATE TABLE edges (
edge_id integer PRIMARY KEY,
tail_vertex integer REFERENCES vertices (vertex_id),
head_vertex integer REFERENCES vertices (vertex_id),
label text,
properties json
);

需要注意的是:

  • 每个节点可以通过一条边连接到另外一个节点,没有任何的存储结构限制。
  • 对于任何一个节点,可以很容易的查出它的所有入边(incoming edges)和出边(outgoing edges),也就是说可以很容易地遍历整个图。
  • 通过使用不同的标签(label),可在一张图里面,存储各种不同类型的信息。

总结

不同的场景有不同的需求,没有一个放之四海而皆准的解决方案,彼之蜜糖也许是吾之砒霜。

在可以预见的未来,关系型数据模型仍将处于主导地位,并且和文档数据模型、图数据模型等多种非关系型数据模型并存。