「数据模型」(Data models)是软件开发中最重要的部分之一,大部分应用程序都是通过数据模型的层层叠加来构建的,例如:
- 应用程序开发人员通过对象或数据结构,以及操作这些数据结构的API来对真实世界建模。
- 数据库开发人员采用通用数据模型(如关系数据库中的表)来存储上述数据结构。
- 数据库工程师决定用何种字节格式来表述上述通用数据模型,数据表示需要支持查询、搜索等操作。
- 硬件工程师需要考虑如何用电流、光脉冲、磁场等来表示字节。
可以看出,每一层都通过提供一个简洁的数据模型来隐藏下层的复杂性,这些抽象机制使得不同的人群可以高效协作。本章节将介绍一系列用于数据存储和查询的「通用数据模型」(对应列表中的第2点)。
1 关系模型与文档模型
当前,以 SQL(结构化查询语言)为代表的「关系模型」(relational model)可能是最著名的数据模型。SQL将数据组织成关系,存储在表(table)中,其中每个关系都是元组(tuples)的无序集合(在 SQL 中称为行)。关系模型的目标是将实现细节隐藏在更简洁的接口后面。进入21世纪,以文档模型与图模型为代表的「非关系数据模型」(NoSQL)开始逐渐涌现并不断发展。采用NoSQL数据库的驱动因素包括比关系数据库更好的扩展性需求、免费与开源、对特定查询操作的支持、不满关系模式的限制性等。本节将重点关注关系模型与文档模型。
1.1 关系模型与文档模型示例
下面将通过一个简历数据模型来说明关系模型与文档模型的差异性。下图给出了在关系模型中表示简历数据的示例。整个简历可以通过唯一的标识符 user_id
来标识,该标识同时也作为其他表的外键来表示简历数据中的一对多关系(职位、教育、联系信息)。
另一方面,通过文档模型(以 JSON 格式为例),简历数据可以被表示为如下所示:
代码语言:javascript复制{
"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 & 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://thegatesnotes.com",
"twitter": "http://twitter.com/BillGates"
}
}
与关系模型相比,文档模型可以减少应用程序代码与存储层之间的阻抗失配(关系模型从数据库到应用层对象需要进行转换,即对象-关系映射),且具有模式灵活性与更好的局部性(之后详述)。总的来看,文档模型对于以一对多关系为主(即「树状结构」)的数据来说较为适合,上述简历数据的树结构如下图所示:
在上面的 JSON 示例中,region_id
与 industry_id
被定义为 ID 而非纯文本字符串形式,这样做的好处是可以将实际的信息只存储在一个地方,引用它的内容都使用 ID,从而消除内容的重复,体现了数据库规范化的思想。然而,这种规范化本质上是一种「多对一」的关系,对于文档模型来说,其通常对「联结」操作支持较弱(即关系数据库中通过外键关联至其他表中的行),导致有时候需要在应用层代码中进行模拟联结。
进一步地,如果要采用「多对多关系」来扩展简历,可以采用如下图所示的数据模型,其中虚线框内的数据可以组织为一个文档,但是指向组织、学校与其他用户的关系需要被表示为引用,且在查询时需要联结操作,相对于关系数据库来说较为不便。
1.2 关系模型与文档模型发展简史
20 世纪 70 年代最受欢迎的商业数据处理库是 IBM 信息管理系统(简称 IMS),其采用了相当简单的数据模型,称为「层次模型」(hierarchical model),其将所有数据表示为嵌套在记录中的记录(树),与 JSON 模型较为相似。IMS 可以较好地支持一对多关系,但是支持多对多关系较为困难,且不支持联结。
为了解决层次模型的局限性,之后又提出了多种解决方案。其中最著名的是「关系模型」(后来演变为 SQL)和「网络模型」(network model)。网络模型由一个称为数据系统语言会议(CODASYL)的委员会进行标准化,也被称为 CODASYL 模型。网络模型是对层次模型的推广,其支持对多对一与多对多的关系进行建模,在联结操作上,网络模型通过类似于编程语言中的指针方式进行实现。访问记录的唯一方法是选择一条始于根记录的路径,并沿着相关链接一次访问,这条链接被称为「访问路径」(access path)。在存在多对多关系的模型中,访问路径需要由应用程序代码进行跟踪,使得数据库的查询与更新变得异常复杂而没有灵活性。
相比之下,关系模型则是定义了所有数据的格式:关系(表)只是元组(行)的集合。没有复杂的嵌套结构,也没有复杂的访问路径。在关系数据库中,由查询优化器自动决定以何种顺序执行查询,以及使用哪些索引。这些选择而实际上等价于访问路径,但它们是由查询优化器自动生成的,而不是由应用开发人员所维护。
对于文档模型来说,从其父记录保存了嵌套记录(一对多关系)而非存储在单独的表中这一角度来看,其可以理解为某种方式的层次模型。但是在表示多对一与多对多关系时,关系数据库与文档数据库中的相关项都是由唯一的标识符引用,该标识符在关系模型中被称为「外键」,而在文档模型中则被称为「文档引用」。标识符可以在读取时通过联结操作或相关后续查询进行解析。
1.3 关系模型与文档模型对比
概括来说,从数据模型本身来看,「文档模型」的主要优势在于模式灵活性与数据局部性,且对于某些应用来说,其更接近于应用程序所使用的数据结构;而「关系模型」的主要优势在于联结操作,以及多对一与多对多关系更简洁的表达上。
具体来说,在「应用代码」层面,如果应用模型具有类似文档的结构(一对多关系树),那么使用文档模型更为合适;而关系模型则倾向于某种数据分解,把文档结构分解为多个表,可能使得模式更为笨重。如果应用程序确实使用了多对多关系,则对联结支持不足的文档模型就显得不太吸引人,虽然可以通过反规范化(复制内容)减少对联结的需求,但是这会增加程序的复杂性。PS:对于高度关联的数据,实际上图模型最为适合。
在「模式灵活性」层面,大部分文档模型允许将任意的键-值添加到文档中,仅在读数据的代码中加以限制,这种模式可以称为「读时模式」(数据结构隐形,只有在读取时才解释),与关系模型的「写时模式」(模式是显式地,数据写入时必须遵循)相对应。不同的模式各有优劣,读时模式可能更适合于可能需要频繁更改数据格式的应用程序,而写时模式更适合于需要记录与确保某种特定数据结构的应用程序。
在「数据局部性」层面,由于文档通常存储为编码为 JSON、XML等形式的连续字符串,如果应用程序需要频繁访问整个文档,则存储局部性具有性能优势;而如果数据被划分在多个表中(关系模型),则需要进行多次索引查找来检索所有数据,可能需要更多的磁盘 I/O 并花费更多的时间。需要注意,局部性优势仅适用需要同时访问文档大部分内容的场景。
随着时间的推移,关系数据库与文档数据库变得越来越相近。大多数关系数据库系统都支持 XML,还有一些关系数据库系统支持了 JSON(说的就是你,MySQL);在文档数据库方面,RethinkDB 的查询接口支持和关系型类似的联结,而一些 MongoDB 驱动程序可以自动解析数据库的引用关系(在客户端执行高效联结)。
2 数据查询语言
2.1 声明式查询与命令式查询
对于关系模型来说,其所包含的数据查询方法 SQL 是一种「声明式」(declarative)查询语言,而文档模型的前身 IMS 和 CODASYL 则是「命令式」(imperative)。
很多常用编程语言都是命令式的,其会告诉计算机以特定顺序执行某些操作,我们可以完全推理出整个过程,例如如下查询代码:
代码语言:javascript复制function getSharks() {
var sharks = [];
for (var i = 0; i < animals.length; i ) {
if (animals[i].family === "Sharks") {
sharks.push(animals[i]);
}
}
return sharks;
}
而对于声明式查询,则只需指定所需的数据模式,结果需要满足什么条件,以及如何转换数据(例如排序、分类),而不需指明如何实现这一目标。例如,上述例子对应的 SQL 查询代码如下:
代码语言:javascript复制SELECT * FROM animals WHERE family = 'Sharks';
和命令式查询相比,声明式查询的优势包括:
- 比命令式更加简洁和容易使用
- 对外隐藏了数据库引擎的实现细节,便于优化性能
- 对并行执行更为友好
2.2 Web 上的声明式查询
本节将以 Web 浏览器为例,对声明式查询语言的优点进行进一步描述。假设有一个关于海洋动物的网站,用户正在查看有关鲨鱼的页面,因此导航项「鲨鱼」被标记为当前网页,如下所示:
代码语言:javascript复制<ul>
<li class="selected">
<p>Sharks</p>
<ul>
<li>Great White Shark</li>
<li>Tiger Shark</li>
<li>Hammerhead Shark</li>
</ul> </li>
<li>
<p>Whales</p>
<ul>
<li>Blue Whale</li>
<li>Humpback Whale</li>
<li>Fin Whale</li>
</ul>
</li>
</ul>
现在我们需要将当前页面的标题设置为蓝色背景,以在视觉上突出,通过 CSS 样式表可以较容易地实现:
代码语言:javascript复制li.selected > p {
background-color: blue;
}
上述 CSS 可以精准匹配到 <p>Sharks</p>
元素,其属于一种声明式语言。而如果需要采用命令式方法,例如 JavaScript 的文档对象模型(DOM)API,则结果可能是这样:
var liElements = document.getElementsByTagName("li");
for (var i = 0; i < liElements.length; i ) {
if (liElements[i].className === "selected") {
var children = liElements[i].childNodes;
for (var j = 0; j < children.length; j ) {
var child = children[j];
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "P") {
child.setAttribute("style", "background-color: blue");
}
}
}
}
上述代码比 CSS 更长,更难理解,同时存在一些严重的问题:
- 如果
selected
类被删除,即使代码重新允许,蓝色也不会移出,直到整个页面并重新加载;而 CSS 将在类被删除后立即清楚蓝色背景 - 如果想利用新的 API,可能会提高效能,但是必须重写代码;而浏览器厂商可以在不破坏兼容性的情况下提高 CSS 的性能
综上所述,在 Web 浏览器中,使用声明式 CSS 样式表比用 JavaScript 命令式地操作样式要好得多。
2.3 MapReduce 查询
「MapReduce」 是一种由 Google 提出的编程模型,用于在许多机器上批量处理海量数据。一些 NoSQL 存储系统(例如 MongoDB 和 CouchDB)支持有限的 MapReduce 方式在大量文档上执行只读查询。本节将简要介绍 MongoDB 对该模型的使用。
MapReduce 既不是声明式查询语言,也不是一个完全命令式的查询 API,而是介于两者之间:查询的逻辑用代码片段表示,这些代码片段可以被处理框架重复进行调用。其主要基于在许多函数式编程语言中存在的 map
和 reduce
函数实现。
下面将通过一个例子对 MapReduce 进行说明:假设你是一名海洋生物学家,每当看到海洋中的动物时,就会在数据库中添加观察记录。现在需要生成一份报告,来说明每个月看到了多少鲨鱼。
在 PostgreSQL(一种关系型数据库)中,该查询可以表达为:
代码语言:javascript复制SELECT date_trunc('month', observation_timestamp) AS observation_month,
sum(num_animals) AS total_animals
FROM observations
WHERE family = 'Sharks'
GROUP BY observation_month;
其中 date_trunc
函数用来将时间戳向下舍入到最近的月份。该查询首先对观察结果进行过滤,仅显示鲨鱼物种,然后按照他们发生的月份对观察结果进行分组,最后将该月所有观察的动物数量求和汇总。
在 MongoDB 中,可以通过 MapReduce 功能实现类似的目的:
代码语言:javascript复制db.observations.mapReduce(
function map() {
var year = this.observationTimestamp.getFullYear();
var month = this.observationTimestamp.getMonth() 1;
emit(year "-" month, this.numAnimals);
},
function reduce(key, values) {
return Array.sum(values);
},
{
query: { family: "Sharks" },
out: "monthlySharkReport"
}
);
- 过滤器声明式地制定鲨鱼种类(这是 MongoDB 对 MapReduce 的特有扩展)
- 对于每个匹配查询的文档,JavaScript 的
map
函数会被调用,将this
设定为文档对象 map
函数发射一个键值对,其中键是由年份和月份组成的字符串,值代表观察的动物数量map
函数发射的键值对按键分组,对于相同键的所有键值对,调用reduce
函数- 最终的输出写入到
monthlySharksReport
集合中
例如,假定观察集合中包含如下两个文档:
代码语言:javascript复制{
observationTimestamp: Date.parse("Mon, 25 Dec 1995 12:34:56 GMT"),
family: "Sharks",
species: "Carcharodon carcharias",
numAnimals: 3
}
{
observationTimestamp: Date.parse("Tue, 12 Dec 1995 16:17:18 GMT"),
family: "Sharks",
species: "Carcharias taurus",
numAnimals: 4
}
每个文档都会调用一次 map
函数,从而产生 emit("1995-12", 3)
和 emit("1995-12", 3)
;随后,reduce
函数将被调用,reduce("1995-12", [3,4])
,返回 「7」。
map
和 reduce
函数对于可执行的操作有所限制,其必须为「纯」函数,只能使用传递进去的数据作为输入,而不能执行额外的数据库查询,也不能有任何的副作用。这些限制使得数据库能够在任何位置,以任意顺序来运行函数,并在失败时重新运行这些函数,从而实现解析字符串、调用库函数、执行计算等功能。
MapReduce 的一个可用性问题是,必须编写两个密切协调的 JaveScript 函数,这通常比编写单个查询更难。此外,声明式查询语言为查询优化器提供了更多提高查询性能的机会。基于上述原因,MongoDB 2.2 增加了被称为「聚合管道」的声明式查询语言支持。在该语言中,鲨鱼计数查询可以实现如下:
代码语言:javascript复制db.observations.aggregate([
{ $match: { family: "Sharks" } },
{ $group: {
_id: {
year: { $year: "$observationTimestamp" },
month: { $month: "$observationTimestamp" }
},
totalAnimals: { $sum: "$numAnimals" }
} }
]);
聚合管道在表达能力上类似于 SQL 的子集,不过其使用了基于 JSON 的语法,而非 SQL 的英语句式语法。
总的来看,MapReduce 是一个相当底层的编程模型,用于在计算集群上分布执行。高层次的查询语言(例如 SQL)可以通过 MapReduce 操作的 pipeline 来实现,但需要注意,分布式查询并不是一定要借助于 MapReduce。同时,在查询中使用 JavaScript 代码是一种高级特性,也不限于 MapReduce。
3 图状数据模型
根据之前的描述,多对多关系是不同数据模型之间的重要区别特征。如果应用大部分是一对多关系(树结构数据)或者记录之间没有关系,那么「文档模型」是最合适的;而如果数据中多对多关系很常见,那么可以使用「关系模型」来处理较简单的多对多情况,但随着数据之间的关联越来越复杂,将数据建模为「图模型」会更加自然。
图由两种对象组成:「顶点」(也称为节点或实体)和「边」(也称为关系或弧)。很多数据可以建模为图,例如:
- 社交网络:顶点是人,边表示哪些人彼此认识
- Web 图:顶点是网页,边表示与其他界面的 HTML 链接
- 公路或铁路网:顶点是交叉路口,边表示它们之间的公路或铁路
除了上述表示相同类型事物外,图还可以表示不同类型的异构数据。本节将使用如下图所示的图,其显示了一对夫妻与其居住地和出生地的情况:
构建和查询图中数据的方法有很多,本节将讨论「属性图」模型(以 Neo4j、Titan 和 InfiniteGraph 为代表)和「三元存储」模型(以 Datomic、AllegroGraph 为代表),并介绍三种声明式图查询语言:Cypher、SPARQL 和 Datalog。
3.1 属性图
在属性图(property graph)模型中,每个顶点包括:
- 唯一的标识符
- 出边的集合
- 入边的集合
- 属性的集合(键值对)
每条边包括:
- 唯一的标识符
- 边开始的顶点(尾部顶点)
- 边结束的顶点(头部顶点)
- 描述两个顶点间关系类型的标签
- 属性的集合(键值对)
我们可以将图存储看作由两个关系表组成,一个用于顶点,一个用于边,如下例所示:
代码语言:javascript复制CREATE TABLE vertices (
vertex_id integer PRIMARY KEY,
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
);
CREATE INDEX edges_tails ON edges (tail_vertex);
CREATE INDEX edges_heads ON edges (head_vertex);
该例子使用了 Postgre SQL 的 json 数据类型来存储每个顶点或边的属性。除了两张表外,还新建了两个索引来查询顶点的入边或出边的集合(head_vertex
和 tail_vertex
)。此外,还需要明确以下特征:
- 任何顶点都可以连接到其他任何顶点,没有模式限制哪种事物可以或不可以关联
- 给定某个顶点,可以高效地得到它的所有入边和出边,从而实现图的遍历
- 通过对不同类型的关系使用不同的标签,可以在单个图中存储多种不同类型的信息,同时保持数据模型的整洁性
3.2 Cypher 查询语言
Cypher 是一种用于属性图的「声明式」查询语言,最早为 Neo4j 图数据库创建。下面的例子展示了将上述属性图示例的左侧插入图数据库的 Cypher 查询。每一个顶点都需指定一个像 USA
或 Idaho
这样的符号名称,同时指明其类型(这里类型应该是预先定义好的,且对于 Person 类其可以直接关联到 type
属性),查询可以使用这些名称创建顶点之间的边,使用箭头符号:(Idaho) -[:WITHIN]-> (USA)
创建了一个标签为 WITHIN
的边,其中 Idaho
为尾结点,USA
为头结点。
CREATE
(NAmerica:Location {name:'North America', type:'continent'}),
(USA:Location {name:'United States', type:'country' }),
(Idaho:Location {name:'Idaho', type:'state' }),
(Lucy:Person {name:'Lucy' }),
(Idaho) -[:WITHIN]-> (USA) -[:WITHIN]-> (NAmerica),
(Lucy) -[:BORN_IN]-> (Idaho)
通过这种查询插入所有的顶点和边后,我们可以继续进行一些高阶查询,例如查找所有从美国移民到欧洲的人员名单,即查找 BORN_IN
边指向美国,而 LIVING_IN
边指向欧洲的所有顶点,然后返回每个这样顶点的 name
属性。我们可以通过如下查询语句实现这一需求:
MATCH
(person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (us:Location {name:'United States'}),
(person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (eu:Location {name:'Europe'})
RETURN person.name
该查询中的 MATCH
语句采用了相同的箭头语义 (person) -[:BORN_IN]-> ()
来匹配任意两个通过 BORN_IN
标签的边所关联的节点,其中尾部顶点绑定至变量 person
,头部顶点则没有要求。该查询的具体解读如下:
person
有一个连接到其他顶点的出边BORN_IN
。从该顶点开始,可以沿着一系列出边WITH_IN
,直到最终到达类型为 Location 的顶点,其name
属性对应的值为 "United States"- 同一个
person
顶点也有一个出边LIVES_IN
。沿着这条边,然后是一系列的出边WITH_IN
,最终到达类型为 Location 的顶点,name
属性为 "Europe"
对于每个这样的 person
顶点,返回其 name
属性。
如之前所述,对于声明式查询语言,在编写语句时不需要指定执行细节,查询优化器会自动选择效率最高的执行策略,因此开发者可以专注于应用的其他部分。
3.2.1 SQL 中的图查询
对于上述查询,如果把图数据放在关系结构中,我们也可以通过 SQL 来实现这种查询。由于需要遍历未知数量的边,因此 join 操作数量是不确定的。Cypher 可以用 :WITHIN*0..
来简洁地表达这个情况,而 SQL 则可以使用「递归共用表表达式」(WITH RECURSIVE
语法)来表示,具体查询如下所示:
WITH RECURSIVE
-- in_usa is the set of vertex IDs of all locations within the United States
in_usa(vertex_id) AS (
SELECT vertex_id FROM vertices WHERE properties->>'name' = 'United States'
UNION
SELECT edges.tail_vertex FROM edges
JOIN in_usa ON edges.head_vertex = in_usa.vertex_id
WHERE edges.label = 'within'
),
-- in_europe is the set of vertex IDs of all locations within Europe
in_europe(vertex_id) AS (
SELECT vertex_id FROM vertices WHERE properties->>'name' = 'Europe'
UNION
SELECT edges.tail_vertex FROM edges
JOIN in_europe ON edges.head_vertex = in_europe.vertex_id
WHERE edges.label = 'within'
),
-- born_in_usa is the set of vertex IDs of all people born in the US
born_in_usa(vertex_id) AS (
SELECT edges.tail_vertex FROM edges
JOIN in_usa ON edges.head_vertex = in_usa.vertex_id
WHERE edges.label = 'born_in'
),
-- lives_in_europe is the set of vertex IDs of all people living in Europe
lives_in_europe(vertex_id) AS (
SELECT edges.tail_vertex FROM edges
JOIN in_europe ON edges.head_vertex = in_europe.vertex_id
WHERE edges.label = 'lives_in'
)
SELECT vertices.properties->>'name'
FROM vertices
-- join to find those people who were both born in the US *and* live in Europe
JOIN born_in_usa ON vertices.vertex_id = born_in_usa.vertex_id
JOIN lives_in_europe ON vertices.vertex_id = lives_in_europe.vertex_id;
概括来看,该查询比 Cypher 查询的行数多很多,足以说明不同的数据模型适用于不同的场景。选择适合应用的数据模型是非常重要的。
3.4 三元存储和 SPARQL
3.4.1 三元存储模型
三元存储模型几乎等同于属性图模型,只是使用不同的名词描述了相同的思想。在三元存储中,所有的信息都以非常简单的三部分形式存储:(「主体」、「谓语」、「客体」),其中主体相当于图中的顶点,而客体则是以下两种之一:
- 原始数据类型中的值,如字符串或数字。在这种情况下,三元组的谓语和客体分别相当于主体(顶点)的一个属性的键和值。例如,(lucy, age, 33) 就好比是顶点 lucy 具有属性
{"age":33}
- 图中的另一个顶点。在这种情况下,主体是尾部顶点,而客体是头部顶点。例如,(lucy, marriedTo, alain) 中主体 lucy 和客体 alian 都是顶点,并且谓语 marriedTo 是连接二者的边的标签
下面的语句以三元组的方式展示了与之前相同的图数据,具体的形式为 Turtle,其属于 Notation3(N3)的一个子集:
代码语言:javascript复制@prefix : <urn:example:>.
_:lucy a :Person; :name "Lucy"; :bornIn _:idaho.
_:idaho a :Location; :name "Idaho"; :type "state"; :within _:usa.
_:usa a :Location; :name "United States"; :type "country"; :within _:namerica.
_:namerica a :Location; :name "North America"; :type "continent".
在上述示例中,图的顶点被写作 _:someName
,这一名字在定义文件以外没有任何意义,只是为了区分三元组的不同顶点。当谓语表示边时,客体是另一个顶点,如 _:idaho :within _:usa
;而当谓语表示一个属性时,客体是一个字符串,如 _:usa :name "United States"
。对于定义相同主体的多个三元组,可以使用分号来说明同一主体的多个对象信息。
3.4.2 语义网与 RDF 数据模型
当深入了解三元存储相关信息后,通常会将其与「语义网」(semantic web)关联起来,实际上,三元存储数据模型是完全独立于语义网的。语义网的初衷是将信息发布为机器可读的数据以供计算机阅读,「资源描述框架」(RDF)就是这样一种机制,它让不同网站以一致的格式发布数据,这样来自不同网站的数据可以自动合并为一个数据网络,形成一种互联网级别的包含万物的数据库。
上面例子使用的 Turtle 语言就是 RDF 数据的一种人类可读格式。有时,RDF 也用 XML 格式来编写,相对来说会冗长一些,如下所示。人眼更容易阅读 Turtle/N3 格式的数据,而 Apache Jena 等工具则可以快速转换不同的 RDF 格式。
代码语言:javascript复制<rdf:RDF xmlns="urn:example:"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<Location rdf:nodeID="idaho">
<name>Idaho</name>
<type>state</type>
<within>
<Location rdf:nodeID="usa">
<name>United States</name>
<type>country</type>
<within>
<Location rdf:nodeID="namerica">
<name>North America</name>
<type>continent</type>
</Location>
</within>
</Location>
</within>
</Location>
<Person rdf:nodeID="lucy">
<name>Lucy</name>
<bornIn rdf:nodeID="idaho"/>
</Person>
</rdf:RDF>
因为旨在为全网数据交换而设计,RDF 存在一些特殊的约定。三元组的主体、谓语和客体通常是 URI。例如,一个谓语可以是一个诸如 <http://my-company.com/namespace#within>
或 <http://my-company.com/namespace#lives_in>
的 URI,而不仅仅是 WITHIN
或 LIVES_IN
。这种设计的考量在于,假设你的数据需要和其他人的数据相结合,万一不同人给单词 within
或 lives_in
赋予了不同的含义,采用 URI 就可以避免冲突,因为别人的谓语会是 <http://other.org/foo#within>
和 <http://other.org/foo#lives_in>
。
从 RDF 的角度来看,URL <http://my-company.com/namespace>
不一定需要解析出特定的内容,其只是一个命名空间(前缀),可以在文件头部指定一次,然后即可忽略它。本文的例子使用了不可解析的 URI,如 urn:example:within
。PS:「URI」(统一资源标识符)包含了 「URL」(统一资源定位符,定位信息资源)与 「URN」(统一资源名称,命名非信息资源)。
3.4.3 SPARQL 查询语言
「SPARQL」 是一种采用 RDF 数据模型的三元存储查询语言,其是 SPARQL Protocol and RDF Query Language 的缩写(好一个套娃),发音为 ”sparkle“。其出现时间早于 Cypher,并且 Cypher 的模式匹配是借鉴自 SPARQL 的,因此二者看上去十分相似。
对于之前的查询(从美国移民到欧洲的人员),SPARQL 比 Cypher 要更加简洁,具体如下:
代码语言:javascript复制PREFIX : <urn:example:>
SELECT ?personName WHERE {
?person :name ?personName.
?person :bornIn / :within* / :name "United States".
?person :livesIn / :within* / :name "Europe".
}
由于 RDF 不区分属性和边,可以同时对两者执行谓语操作,采用相同的语法来匹配属性上的条件。总的来说,SPARQL 是一种非常优秀的查询语言,可以成为应用程序内部使用的强大查询工具。
3.5 Datalog 语言
「Datalog」 是比 SPARQL 或 Cypher 更为古老的语言,其出现于 20 世纪 80 年代,为之后的查询语言奠定了基础。在实践中,Datalog 语言被应用在多个数据系统中,例如 Datomic 系统将其作为查询语言;Hadoop 则基于 Datalog 实现了 Cascalog 用于大数据集的查询。
Datalog 的数据模型类似于三元组存储模型,但更为通用一些。其采用「谓语(主体,客体)」,而不是三元组的 (主体,谓语、客体),如下所示:
代码语言:javascript复制name(namerica, 'North America').
type(namerica, continent).
name(usa, 'United States').
type(usa, country).
within(usa, namerica).
name(idaho, 'Idaho').
type(idaho, state).
within(idaho, usa).
name(lucy, 'Lucy').
born_in(lucy, idaho).
基于上述模型,我们可以实现与之前相同的查询,其看上去与 Cypher 或 SPARQL 有较大差别:
代码语言:javascript复制within_recursive(Location, Name) :- name(Location, Name). /* Rule 1 */
within_recursive(Location, Name) :- within(Location, Via), /* Rule 2 */
within_recursive(Via, Name).
migrated(Name, BornIn, LivingIn) :- name(Person, Name), /* Rule 3 */
born_in(Person, BornLoc),
within_recursive(BornLoc, BornIn),
lives_in(Person, LivingLoc),
within_recursive(LivingLoc, LivingIn).
?- migrated(Who, 'United States', 'Europe').
/* Who = 'Lucy'. */
Datalog 将查询拆解为多个规则,通过对新谓语的定义与引用来实现递归查询。在规则中,以大写字母开头的单词是变量,谓词的匹配则与 Cypher 和 SPARQL 一样。如果系统可以在操作符 :-
的右侧找到与所有谓词的匹配项,则规则适用。当规则适用时,就将操作符左侧的变量替换为它们匹配的值。
通过反复应用上述查询中的规则 1 和规则 2,within_recursive
谓词可以返回数据库中包含的所有位于 North America 的地点(或任何其他地点名称),如下图所示:
基于规则 1 和规则 2, 规则 3 可以找到出生在某个地方 BornIn
且居住在某个地方 LivingIn
的所有人,并将此人作为变量 Who
,最终得到和之前相同的查询结果。
4 小结
本章节对几种主要的数据模型进行了概括性的介绍。历史上,数据最初被表示为一棵大树(层次模型),但是这不利于表示多对多的关系,所以发明了「关系模型」来解决这个问题;而最近,开发人员发现一些应用也不太适合关系模型,于是又出现了新的非关系 NoSQL 数据存储,其主要分为两个方向:
- 「文档数据库」的目标用例是产生于自包含文档中的数据,其中一个文档与其他文档之间的关联较少
- 「图数据库」针对相反的场景的目标用例是所有数据都可能会相互关联
上述三种模型如今都有着广泛的应用,并且在各自的目标领域表现优异。三者之间并不是完全独立的:一个模型可以用另外一个模型来模拟,但是结果通常是比较繁琐的,这也解释了为什么针对不同的目的需要应用不同的数据模型。此外,每种数据模型都有自己的「查询语言」或框架,本章讨论了几个例子:SQL、MapReduce、MongoDB 的聚合管道、Cypher、SPARQL 和 Datalog。
当然,还有一些数据模型尚未提及,例如基因组数据库、超大规模数据分析定制模型、全文搜索数据模型等。在下一章中,我们将讨论在实现本章所描述的数据模型的过程中有哪些重要的权衡设计。