一个复杂的应用程序可能会有更多的中间层次,比如基于API的API,不过基本思想仍然是一样的:每个层都通过提供一个明确的数据模型来隐藏更低层次中的复杂性。这些抽象允许不同的人群有效地协作。
数据模型种类繁多,每个数据模型都带有如何使用的设想。有些用法很容易,有些则不支持如此;有些操作运行很快,有些则表现很差;有些数据转换非常自然,有些则很麻烦。
掌握一个数据模型需要花费很多精力。即便只使用一个数据模型,不用操心其内部工作机制,构建软件也是非常困难的。然而,因为数据模型对上层软件的功能(能做什么,不能做什么)有着至深的影响,所以选择一个适合的数据模型是非常重要的。
1.关系模型与文档模型
关系模型曾是一个理论性的提议,当时很多人都怀疑是否能够有效实现它。然而到了20世纪80年代中期,关系数据库管理系统(RDBMSes)和SQL已成为大多数人们存储和查询某些常规结构的数据的首选工具。关系数据库已经持续称霸了大约25~30年——这对计算机史来说是极其漫长的时间。
2010年代,NoSQL开始了最新一轮尝试,试图推翻关系模型的统治地位。“NoSQL”这个名字让人遗憾,因为实际上它并没有涉及到任何特定的技术。最初它只是作为一个醒目的Twitter标签,用在2009年一个关于分布式,非关系数据库上的开源聚会上。无论如何,这个术语触动了某些神经,并迅速在网络创业社区内外传播开来。好些有趣的数据库系统现在都与#NoSQL#标签相关联,并且NoSQL被追溯性地重新解释为不仅是SQL(Not Only SQL)。
采用NoSQL数据库的背后有几个驱动因素,其中包括:
- 需要比关系数据库更好的可扩展性,包括非常大的数据集或非常高的写入吞吐量
- 相比商业数据库产品,免费和开源软件更受偏爱。
- 关系模型不能很好地支持一些特殊的查询操作
- 受挫于关系模型的限制性,渴望一种更具多动态性与表现力的数据模型
不同的应用程序有不同的需求,一个用例的最佳技术选择可能不同于另一个用例的最佳技术选择。关系数据库似乎可能会继续与各种非关系数据库一起使用 - 这种想法有时也被称为混合持久化(polyglot persistence)。
1.1.对象如何存储
目前大多数应用程序开发都使用面向对象的编程语言来开发,这导致了对SQL数据模型的普遍批评:如果数据存储在关系表中,那么需要一个笨拙的转换层,处于应用程序代码中的对象和表,行,列的数据库模型之间。
像ActiveRecord和Hibernate这样的 对象关系映射(ORM object-relational mapping) 框架可以减少这个转换层所需的样板代码的数量,但是它们不能完全隐藏这两个模型之间的差异。
上图展示了如何在关系模式中表示简历(一个LinkedIn简介)。整个简介可以通过一个唯一的标识符user_id来标识。像first_name和last_name这样的字段每个用户只出现一次,所以可以在User表上将其建模为列。但是,大多数人在职业生涯中拥有多于一份的工作,人们可能有不同样的教育阶段和任意数量的联系信息。从用户到这些项目之间存在一对多的关系,可以用多种方式来表示:
- 传统SQL模型中,最常见的规范化表示形式是将职位,教育和联系信息放在单独的表中,对User表提供外键引用。
- 后续的SQL标准增加了对结构化数据类型和XML数据的支持;这允许将多值数据存储在单行内,并支持在这些文档内查询和索引。这些功能在Oracle,IBM DB2,MS SQL Server和PostgreSQL中都有不同程度的支持。JSON数据类型也得到多个数据库的支持,包括IBM DB2,MySQL和PostgreSQL。
- 第三种选择是将职业,教育和联系信息编码为JSON或XML文档,将其存储在数据库的文本列中,并让应用程序解析其结构和内容。这种配置下,通常不能使用数据库来查询该编码列中的值。
对于一个像简历这样自包含文档的数据结构而言,JSON表示是非常合适的,面向文档存储的数据库如MongoDB可以很好支持这种数据模型:
{
"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表示使得这个树状结构变得明确:
1.2.读时模型&写时模式
文档数据库的本质是读时模式(schema-on-read),数据的结构是隐含的,只有在数据被读取时才被解释)。相应的关系式数据库是写时模式(schema-on-write),传统的关系数据库方法中,模式明确,且数据库确保所有的数据都在写时符合其模式。
读时模式类似于编程语言中的动态(运行时)类型检查,而写时模式类似于静态(编译时)类型检查。
1.3.文档查询数据局部性
文档通常以单个连续字符串形式进行存储,编码为JSON,XML或其二进制变体(如MongoDB的BSON)。如果应用程序经常需要访问整个文档(例如,将其渲染至网页),那么存储局部性会带来性能优势,因为不需要进行过多的连表连接。
如果将数据分割到多个表中,则需要进行多次索引查找才能将其全部检索出来,这可能需要更多的磁盘查找并花费更多的时间。
1.4.文档和关系数据库融合
大多数关系数据库系统(MySQL除外)都已支持XML。这包括对XML文档进行本地修改的功能,以及在XML文档中进行索引和查询的功能。这允许应用程序使用那种与文档数据库应当使用的非常类似的数据模型。
从9.3版本开始的PostgreSQL,从5.7版本开始的MySQL以及从版本10.5开始的IBM DB2也对JSON文档提供了类似的支持级别。鉴于用在Web APIs的JSON流行趋势,其他关系数据库很可能会跟随他们的脚步并添加JSON支持。
随着时间的推移,关系数据库和文档数据库似乎变得越来越相似,这是一件好事:数据模型相互补充,如果一个数据库能够处理类似文档的数据,并能够对其执行关系查询,那么应用程序就可以使用最符合其需求的功能组合。
2.数据查询语言
2.1.命令式及声明式查询语言
当引入关系模型时,关系模型包含了一种查询数据的新方法:SQL是一种 声明式 查询语言,而IMS和CODASYL使用 命令式 代码来查询数据库。
- 许多常用的编程语言是命令式的。例如,给定一个动物物种的列表,返回列表中的鲨鱼可以这样写
function getSharks() {
var sharks = [];
for (var i = 0; i < animals.length; i++) {
if (animals[i].family === "Sharks") {
sharks.push(animals[i]);
}
}
return sharks;
}
- SQL,声明式查询,屏蔽查询引擎的细节
SELECT * FROM animals WHERE family ='Sharks';
命令式语言告诉计算机以特定顺序执行某些操作。可以想象一下,逐行地遍历代码,评估条件,更新变量,并决定是否再循环一遍。
在声明式查询语言(如SQL或关系代数)中,你只需指定所需数据的模式 - 结果必须符合哪些条件,以及如何将数据转换(例如,排序,分组和集合) - 但不是如何实现这一目标。数据库系统的查询优化器决定使用哪些索引和哪些连接方法,以及以何种顺序执行查询的各个部分。
2.2.MapReduce查询
MapReduce是一个由Google推广的编程模型,用于在多台机器上批量处理大规模的数据集。一些NoSQL数据存储(包括MongoDB和CouchDB)支持有限形式的MapReduce,作为在多个文档中执行只读查询的机制。
MapReduce既不是一个声明式的查询语言,也不是一个完全命令式的查询API,而是处于两者之间:查询的逻辑用代码片断来表示,这些代码片段会被处理框架重复性调用。它基于map(也称为collect)和reduce(也称为fold或inject)函数,两个函数存在于许多函数式编程语言中。
假设你是一名海洋生物学家,每当你看到海洋中的动物时,你都会在数据库中添加一条观察记录。现在你想生成一个报告,说明你每月看到多少鲨鱼。
这个查询首先过滤观察记录,以只显示鲨鱼家族的物种,然后根据它们发生的日历月份对观察记录果进行分组,最后将在该月的所有观察记录中看到的动物数目加起来。
使用MongoDB的MapReduce查询的编写方式:
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"
});
- 可以声明式地指定只考虑鲨鱼种类的过滤器(这是一个针对MapReduce的特定于MongoDB的扩展)。
- 每个匹配查询的文档都会调用一次JavaScript函数map,将this设置为文档对象。
- map函数发出一个键(包括年份和月份的字符串,如"2013-12"或"2014-1")和一个值(该观察记录中的动物数量)。
- map发出的键值对按键来分组。对于具有相同键(即,相同的月份和年份)的所有键值对,调用一次reduce函数。
- reduce函数将特定月份内所有观测记录中的动物数量相加。
- 将最终的输出写入到monthlySharkReport集合中。
假设observations集合包含这两个文档:
{
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”,4)
- reduce(“1995-12”,[3,4])调用reduce函数,将返回7。
2.3.图数据查询
如果你的应用程序大多数的关系是一对多关系(树状结构化数据),或者大多数记录之间不存在关系,那么使用文档模型是合适的。
要是多对多关系在你的数据中很常见呢:关系模型可以处理多对多关系的简单情况,但是随着数据之间的连接变得更加复杂,将数据建模为图形显得更加自然。
一个图由两种对象组成:顶点(vertices)(也称为节点(nodes) 或实体(entities)),和边(edges)( 也称为关系(relationships) )。多种数据可以被建模为一个图形。典型的例子包括:
- 社交图谱
顶点是人,边表示的是人与人的关系(朋友、亲戚、上下级等) - 网络图谱
顶点是网页,边表示的是网页之间的引用及被引用关系 - 公路或铁路网络
顶点是交叉路口,边线代表它们之间的道路或铁路线。
2.3.1.属性图
在属性图模型中,每个顶点(vertex)包括:
- 唯一的标识符
- 一组 出边(outgoing edges)
- 一组 入边(ingoing edges)
- 一组属性(键值对)
每条 边(edge) 包括: - 唯一标识符
- 边的起点/尾部顶点(tail vertex)
- 边的终点/头部顶点(head vertex)
- 描述两个顶点之间关系类型的标签
- 一组属性(键值对)
可以将图存储看作由两个关系表组成:一个存储顶点,另一个存储边。如果你想要一组顶点的输入或输出边,你可以分别通过head_vertex或tail_vertex来查询edges表
// 顶点表记录顶点信息
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
);
2.3.2.Cypher查询语言
Cypher是属性图的声明式查询语言,为Neo4j图形数据库而发明。
一、顶点及边声明:
- 顶点声明:(顶点名称:顶点类型 {key, value…})
- 边声明:(顶点名称) -[:边关系] -> (顶点名称)…
例如,小明出生与广州,而广州位于广东省,广东省位于中国,那么可以抽象出: - 两类顶点:人物(小明)与位置(广州、广东省、中国)
- 四个顶点:小明、广州、广东省、中国
- 三条条边:小明与广州的关系、广州与广东省的关系,广东省与中国的关系
CREATE
(xiaoMing:person {
'name':'小明'})
(guangZhou:location {
'name':'广州'})
(guangDong:location {
'name':'广东省'})
(china:location {
'name':'中国'})
(xiaoMing) -[:bornIn] -> (guangZhou)
(guangZhou) -[:in] -> (guangDong) -[:in] -> (guangDong) -[:in] -> (china)
二、查询小明所有居住在广东省的朋友
MATCH
(person) -[:friend*0..] -> (person{
'name':'小明'}), -- 首先查询小明的朋友顶点
(person) -[:in*0..] -> (location{
'name':'广东省'}). -- 在查询位于广东省的朋友
3.小结
在历史上,数据最开始被表示为一棵大树(层次数据模型),但是这不利于表示多对多的关系,所以发明了关系模型来解决这个问题。最近,开发人员发现一些应用程序也不适合采用关系模型。新的非关系型“NoSQL”数据存储在两个主要方向上存在分歧:
- 文档数据库的应用场景是:数据通常是自我包含的,而且文档之间的关系非常稀少。
- 图形数据库用于相反的场景:任意事物都可能与任何事物相关联。
这三种模型(文档,关系和图形)在今天都被广泛使用,并且在各自的领域都发挥很好。一个模型可以用另一个模型来模拟 — 例如,图数据可以在关系数据库中表示 — 但结果往往是糟糕的。这就是为什么我们有着针对不同目的的不同系统,而不是一个单一的万能解决方案。
文档数据库和图数据库有一个共同点,那就是它们通常不会为存储的数据强制一个模式,这可以使应用程序更容易适应不断变化的需求。但是应用程序很可能仍会假定数据具有一定的结构;这只是模式是明确的(写入时强制)还是隐含的(读取时处理)的问题。