(点击
上方公众号
,可快速关注)
转自:知了小巷-知了笔记
【导读】:大多数应用程序是通过一层一层叠加数据模型来构建的。每一层都面临的关键问题是:「如何将其用下一层来表示?」复杂的应用程序可能会有更多的「中间层」,例如「基于API来构建上层API」,但是基本思想相同:「每层都通过提供一个简洁的数据模型来隐藏下层的复杂性」。
使团队的不同人群能够高效协作。本文围绕
文档模型、关系模型和图模型
三种模型及语言进行细致展开,欢迎有需要的同学细致品读。
---以下是正文---
数据密集型系统基础:数据模型与查询语言
三言两语
这个世界所有的一切都是表达方式(包括世界本身)...可以叫它模型、可以叫它语言、可以叫它各种各样的符号图形等等。
数据模型可能是开发软件最重要的部分,它们不仅对软件的编写方式,而且还对如何思考待解决的问题都有深远的影响。大多数应用程序是通过一层一层叠加数据模型来构建的。每一层都面临的关键问题是:
「如何将其用下一层来表示?」
例如:
-
作为一名应用程序开发人员,观测现实世界(其中包括人员、组织、货物、行 为、资金流动、传感器等),通过
「对象或数据结构,以及操作这些数据结构的API」
来对其建模。这些数据结构往往特定于该应用。
-
当需要存储这些数据结构时,可以采用
「通用数据模型」
(例如JSON或XML文档、关系数据库中的表或图模型)来表示。
-
数据库工程师接着决定用何种
「内存、磁盘或网络的字节格式」
来表示上述JSON/XML/关系/图形数据。数据表示需要支持多种方式的査询、搜索、操作和处理数据。
-
在更下一层,硬件工程师则需要考虑用
「电流、光脉冲、磁场」
等来表示字节。
复杂的应用程序可能会有更多的
「中间层」
,例如
「基于API来构建上层API」
,但是基本思想相同:
「每层都通过提供一个简洁的数据模型来隐藏下层的复杂性」
。这些抽象机制使得不同的人群可以高效协作,例如数据厂商的工程师和使用数据库的应用程序开发人员一起合作。
关系模型与文档模型
「关系模型」
现在最著名的数据模型可能是SQL,它基于Edgar Codd于1970年提出的
「关系模型:数据被组织成关系(relations)」
,在SQL中称为表(table),其中每个关系都是元组 (tuples)的无序集合(在SQL中称为行)。
关系模型的实现,即关系数据库管理系统(RDBMS)和SQL。
关系数据库的核心在于商业数据处理,20世纪60年代和70年代主要运行在大型计算机之上。从今天的角度来看,用例看起来很常见,主要是
「事务处理」
(包括输入销售和银 行交易、航空公司订票、仓库库存)和
「批处理」
(例如客户发票、工资单、报告)。
关系模型的目标就是
「将实现细节隐藏在更简洁的接口后面」
。
随着计算机变得越来越强大和网络化,服务目的日益多样化。
「关系数据库超出了它们最初的商业数据处理范围」
,顺利推广到了各种各样的用例。当前在网上看到的大部分内容很多仍然是由关系数据库所支撑的,无论是在线发布、论坛、社交网络、电子商务、游戏、SaaS等。
「NoSQL」
“NoSQL”这个名字是不恰当的,因为它其实并不代表具体的某些技术,它最初只是作为一个吸引人眼球的标签。现在很多新兴的数据库系统总是会打上NoSQL的标签,而其含义也已经被逆向重新解释为 “
「不仅仅是SQL」
” 。
采用NoSQL数据库有这样几个驱动因素,包括:
-
比关系数据库
「更好的扩展性」
需求,包括
「支持超大数据集或超高写入吞吐量」
。
-
普遍偏爱
「免费和开源」
软件而不是商业数据库产品。
-
关系模型不能很好地支持一些
「特定的查询操作」
。
-
对关系模式一些限制性感到沮丧,渴望
「更具动态和表达力的数据模型」
不同的应用程序有不同的需求,某个用例的最佳的技术选择未必适合另一个用例。因此,在可预见的将来,
「关系数据库可能仍将继续与各种非关系数据存储一起使用」
,这种思路有时也被称为
「混合持久化」
。
「对象-关系不匹配」
现在大多数应用开发都采用面向对象的编程语言,由于兼容性问题,普遍对SQL数据模型存在抱怨:如果数据存储在关系表中,那么应用层代码中的对象与表、行和列的数据库模型之间需要一个笨拙的转换层。
「Hibernate、MyBatis、Ebean」
这样的对象-关系映射(ORM)框架则减少了此转换层所需的样板代码量,但是他们并不能完全隐藏两个模型之间的差异。
上图展示了如何
「在关系模式中表示一份简历」
。
整个简历可以通过唯一的标识符user_id来标识。像first_name和last_name这样的字段在每个用户中只出现一次,所以可以将其建模为users表中的列。然而,大多数人在他们的职业(职位)中有一个以上的工作,并且可能有多个教育阶段和任意数量的联系信息。用户与这些项目之间存在一对多的关系,可以用多种方式来表示:
-
在传统的SQL模型(SQL:1999之前)中,最常见的规范化表示是将职位、教育和联系信息放在单独的表中,并使用外键引用users表。
-
之后的SQL标准增加了对结构化数据类型和XML数据的支持。这允许将多值数据存储在单行内,并支持在这些文档中査询和索引。一些数据库也支持JSON数据类型,例如IBM DB2, MySQL和PostgreSQL。
-
第三个选项是将工作、教育和联系信息编码为JSON或XML文档,将其存储在数据库的文本列中,并由应用程序解释其结构和内容。对于此方法,通常不能使用数据库査询该编码列中的值。
对于像简历这样的数据结构,它主要是
「一个自包含的文档(document)」
,因此用JSON表示非常合适。与XML相比,JSON的吸引力在于它更简单。面向文档的数据库,如MongoDB等都支持该数据模型。
将Linkedln简历表示为JSON文档,如下图:
「JSON表示」
比
「多表模式」
具有
「更好的局部性」
。如果要在关系模式中读取一份简历,那么要么执行多个査询(通过user_id査询每个表),要么在users表及其从属表之间执行混乱的多路连接join。而对于JSON表示方法,所有的相关信息都在一个地方,一次査询就够了。
用户简历到用户的职位、教育历史和联系信息的一对多关系,意味着数据存在树状结构,JSON将该树结构显式化。
一对多的关系形成了树状结构:
「多对一与多对多的关系」
上面简历中是把region_id和industry_id 定义为ID ,而不是纯文本字符串形式,例如"Greater Seattle Area” 和 “Philanthropy” 。为什么这样做呢?
如果用户界面是可以输入地区或行业的自由文本段,则将其存储为纯文本符串更有意义。但是,拥有地理区域和行业的标准化列表,并让用户从下拉列表或自动填充器中进行选择会更有优势,这样:
-
-
-
易于更新:名字只保存一次,因此,如果需要改变(例如,由于政治事件而更改城市名称),可以很容易全面更新。
-
本地化支持:当网站被翻译成其他语言时,标准化的列表可以方便本地化,因此地区和行业可以用查看者的母语来显示。
-
更好的搜索支持:例如,搜索华盛顿州的慈善家可以匹配到这个简历,这是因为地区列表可以将西雅图属于华盛顿的信息编码进来(而从“大西雅图地区”字符串中并不能看出来西雅图属于华盛顿)。
无论是存储ID还是文本字符串,都涉及
「内容重复的问题」
。当使用ID时,对人类有意义 的信息只存储在一个地方,引用它的所有内容都使用ID(ID只在数据库中有意义)。使用ID的好处是,因为它对人类没有任何直接意义,所以永远不需要直接改变:即使ID标识的信息发生了变化,它也可以保持不变。
「任何对人类有意义的东西都可能在将来某个时刻发生变更。」
如果这些信息被复制,那么所有的冗余副本也都需要更新。这会导致更多写入开销,并且存在数据不一致的风险(信息的一些副本被更新,而其他副本未更新)。消除这种重复正是数据库规范化的核心思想。
这种数据规范化需要表达多对一的关系(许多人生活在同一地区,许多人在同一行业工作),这并不是很适合文档模型。对于关系数据库,由于支持联结操作,可以很方便地通过ID来引用其他表中的行。而在文档数据库中,一对多的树状结构不需要联结,支持联结通常也很弱。如果数据库本身不支持联结,则必须在应用程序代码中,通过对数据库进行多次查询来模拟联结。
即使应用程序的初始版本非常适合采用无联结的文档模型,但随着应用支持越来越多的功能,数据也变得更加互联一体化。
「如何最佳表示数据关系」
「网络模型」
网络模型由一个称为数据系统语言会议(Conference on Data System Languages, CODASYL)的委员会进行标准化,并由多个不同的数据库厂商实施,它也被称为CODASYL模型。
「关系模型」
关系模型所做的则是定义了所有数据的格式:关系(表)只是元组(行)的集合,仅此而已。没有复杂的嵌套结构,也没有复杂的访问路径。可以读取表中的任何一行或者所有行,支持任意条件査询。可以指定某些列作为键并匹配这些列来读取特定行。可以在任何表中插入新行,而不必担心与其他表之间的外键关系。
「在关系数据库中,査询优化器自动决定以何种顺序执行査询,以及使用哪些索引。」
不管怎样,关系模型的一个核心要点是:只需构建一次査询优化器,然后使用该数据库的所有应用程序都可以从中受益。
「RDBMS与文档数据库的比较」
文档数据库是某种方式的层次模型:即在其父记录中保存了嵌套记,如positions, education和contact_info),而不是存储在单独的表中。但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项都由唯一的标识符引用,该标识符在关系模型中被称为外键,在文档模型中被称为文档引用。标识符可以査询时通过联结操作或相关后续査询来解析。迄今为止,文档数据库并未遵循CODASYL标准。
「关系数据库与文档数据库现状」
在比较关系数据库与文档数据库时,需要考虑很多方面的差异,包括它们的容错性和并发处理。
支持文档数据模型的主要论点是模式灵活性,由于局部性而带来较好的性能,对于某些应用来说,它更接近于应用程序所使用的数据结构。关系模型则强在联结操作、多对一和多对多关系更简洁的表达上,与文档模型抗衡。
「哪种数据模型的应用代码更简单?」
如果应用数据具有类似文档的结构(即一对多关系树,通常一次加载整个树),那么使用文档模型更为合适。而关系型模型则倾向于某种数据分解,它把文档结构分解为多个表,有可能使得模式更为笨重,以及不必要的应用代码复杂化。
文档模型也有一定的局限性:例如,
「不能直接引用文档中的嵌套项」
,而需要说“用户251的职位列表中的第二项”(非常类似于层次模型中的访问路径)。然而,只要文档嵌套不太深,这通常不是问题。
在文档数据库中,对联结的支持不足是否是问题取决于应用程序。例如,在使用文档数据库记录事件发生时间的应用分析程序中,可能永远不需要多对多关系。
通常无法一概而论哪种数据模型的应用代码更简单。这主要取决于数据项之间的关系类型。对于高度关联的数据,文档模型不太适合,关系模型可以胜任,而图模型则是最为自然的。
「文档模型中的模式灵活性」
大多数文档数据库,以及关系数据库中的JSON支持,都不会对文档中的数据强制执行任何模式。关系数据库中的XML通常支持带有可选的模式验证功能。没有模式意味着可以将任意的键-值添加到文档中,并且在读取时,客户端无法保证文档可能包含哪些字段。
文档数据库有时被称为无模式,但这具有误导性,因为读数据的代码通常采用某种结构因而存在某种
「隐形模式」
,而不是由数据库强制执行,更准确的术语应该是
「读时模式(数据的结构是隐式的,只有在读取时才解释)」
,与
「写时模式(关系数据库的一种传统方法,模式是显式的,并且数据库确保数据写入时都必须遵循)」
相对应】。
「读时模式类似编程语言中的动态(运行时)类型检査,而写时模式类似于静态(编译时)类型检査。」
正如静态与动态类型检査的支持者对于它们的优缺点存在很大的争议一样,数据库的模式执行也是一个有争议的话题,通常没有明确正确或错误的答案。
当应用程序需要改变数据格式时,这些方法之间的差异就变得尤其明显。
例如,当前用户的全名存储在一个字段中,而现在想
「分别存储名字和姓氏」
。在文档数据库中,只需使用新字段来编写新文档,并在应用层来处理读取旧文档的情况。
if (user && user.name && !user.first_name) (
// 之前写的文档,不存在first_name
user.first_name = user.name.split(" ")[0];
}
而对于“静态类型”数据库模式中,通常会按照以下方式执行升级(migration):
ALTER TABLE users
ADD COLUMN first_name text;
UPDATE users SET first_name = split_part(name,' ', 1); --PostgreSQL
UPDATE users SET first_name = substring_index(name,' ', 1); --MySQL
模式更改由于速度慢并且需要停机,因而评价不高。但这种坏名声其实并不太公平:
「大多数关系数据库系统可以在几毫秒内执行ALTER TABLE语句」
。MySQL则需要注意, 它执行ALTER TABLE会把当前的整张表复制一遍,因而当表很大时可能会需要分钟甚 至几小时的停机时间,尽管现在有各种辅助工具可以解决这个限制。
在大表上运行UPDATE 语句,对于任何数据库都可能会很慢,因为每一行都需要重写。如果这是不可接受的,应用程序可以将first_name置为默认值NULL,并在读取时填 充它,就像使用文档数据库一样。
「查询的数据局部性」
文档通常存储为编码为JSON、XML或其二进制变体(如MongoDB的BSON)的连续字符串。如果
「应用程序需要频繁访问整个文档(例如,在网页上呈现它),则存储局部性具有性能优势」
。如果数据被划分在多个表中,则需要进行多次索引査找来检索所有数据,中间可能需要更多的磁盘I/O并花费更多的时间。
局部性优势仅适用需要同时访问文档大部分内容的场景。由于数据库通常会加载整个文档,如果应用只是访问其中的一小部分,则对于大型文档数据来讲就有些浪费。对文档进行更新时,通常会重写整个文档,而只有修改量不改变源文档大小时,原地覆盖更新才更有效。因此,通常建议文档应该尽量小且
「避免写入时增加文档大小」
。这些性能方面的不利因素大大限制了文档数据库的适用场景。
「文档数据库与关系数据库的融合」
PostgreSQL9.3+、MySQL5.7+都对JSON文档提供了相应支持。随着时间的推移,似乎关系数据库与文档数据库变得越来越相近,或许这是一件好事:数据模型可以相互补充注,如果数据库能够很好处理文档类数据,还能对其执行关系査询,那么应用程序可以使用最符合其需求的功能的组合。融合关系模型与文档模型是未来数据库发展的一条很好的途径。
数据查询语言
当关系模型最初被引入时,就包含了査询数据的新方法:SQL是一种声明式査询语言,而IMS和CODASYL则是命令式。这种差别意味着什么呢?
很多常用的编程语言都是命令式。例如,如果有一个动物物种的列表,可能会写这样的代码来査询列表中的鲨鱼:
function getSharks() {
var sharks =[];
for (var i = 0; i if (animals[i].family === "Sharks") {
sharks.push(animals[i]);
}
)
return sharks;
}
而对于关系代数,则会写成这样:
其中 σ(希腊字母σ)是选择操作符,只返回符合条件的那些动物。SQL遵循上述关系代数的结构:
SELECT * FROM animals WHERE family = 'Sharks';
命令式语言告诉计算机以特定顺序执行某些操作。完全可以推理整个过程,逐行遍历代码、评估相关条件、更新对应的变量,并决定是否再循环一遍。
而对于声明式査询语言(如SQL或关系代数),则只需指定所需的数据模式,结果需满足什么条件,以及如何转换数据(例如,排序、分组和聚合),而不需指明如何实现这一目标。数据库系统的査询优化器会决定采用哪些索引和联结,以及用何种顺序来执行査询的各个语句。
声明式查询语言很有吸引力,它比命令式API更加简洁和容易使用。但更重要的是,它对外隐藏了数据库引擎的很多实现细节,这样数据库系统能够在不改变査询语句的情况下提高性能。
SQL示例则不保证任何特定的顺序,所以顺序改变与否并不重要。但如果查询是命令式代码编写的,那么数据库永远无法确定代码是否依赖于排序。SQL在功能上有更多限制的事实,也给数据库提供了更多自动优化的空间。
最后,声明式语言通常适合于并行执行。现在CPU主要通过增加核,而不是通过比之前更高的时钟频率来提升速度。而命令式代码由于指定了特定的执行顺序,很难在多核和多台机器上并行化。声明式语言则对于并行执行更为友好,它们仅指定了结果所满足的模式,而不指定如何得到结果的具体算法。所以如果可以的话,数据库都倾向于采用并行方式实现查询语言。
Web上的声明式查询
「声明式査询语言的优点不仅限于数据库。」
li.selected > p { background-color: blue;}
比如网页元素选择,可以通过XPath表达式,相当于CSS选择器,都是声明式语言。如果走命令式API,通过JavaScript使用核心文档对象模型(Document Object Model, DOM) API,最后结果可能是这样:
var liElements = document.getElementsByTagName("li");
for (var i = 0; i if (liElements[i].className === "selected") {
var children = liElements[i].childNodes;
for (var j = 0; j var child = children]j];
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "P") {
child.setAttribute("style", "background-color: blue");
}
}
}
}
使用上述JS代码存在的问题:
-
如果selected类被删除(例如,因为用户点击了不同的页面),那么即使代码重新运行,蓝色也不会被移除,因此该项将始终保持高亮显示,直到整个页面被重新加载。而使用CSS,浏览器会自动检测li.selected> p规则何时不再适用,并在selected类被删除后立即清除蓝色背景。
-
如果想利用新的API,比如 document.getElementsByClassName("selected"),或者document.evaluate(),可能会提高性能,但是必须重写代码。另一方面,浏览器厂商可以在不破坏兼容性的情况下提高CSS和XPath的性能。
对于Web浏览器的例子,使用声明式CSS样式表比用JavaScript命令式地操作样式好得 多。类似地,在数据库中,像SQL这样的声明式査询语言比命令式査询APIs要好得多。
MapReduce查询
「MapReduce是一种编程模型,用于在许多机器上批量处理海量数据,兴起于Google」
。一些NoSQL存储系统(例如MongoDB和CouchDB)支持有限的MapReduce方式在大量文档上执行只读查询。
MapReduce既不是声明式査询语言,也不是一个完全命令式的查询API,而是介于两者之间:查询的逻辑用代码片段来表示,这些代码片段可以被处理框架重复地调用。它主要基于许多函数式编程语言中的map (也称为collect)和reduce (也称为fold或inject)函数。
map和reduce函数对于可执行的操作有所限制。它们必须是纯函数,这意味着只能使用传递进去的数据作为输入,而不能执行额外的数据库査询,也不能有任何扩展。这些限制使得数据库能够在任何位置、以任意顺序来运行函数,并在失败时重新运行这些函数。不管怎样,该功能非常强大,可以通过它来解析字符串、调用库函数、执行计算等。
MapReduce是一个相当底层的编程模型,用于在计算集群上分布式执行。而SQL这样的更高层次的査询语言可以通过一些MapReduce操作pipeline来实现。
「当然也有很多SQL的分布式实现并不借助MapReduce。请注意,SQL并没有任何限制 规定它只能在单个机器上运行,而MapReduce也并非垄断了分布式查询。」
图状数据模型(简介)
「多对多关系是不同数据模型之间的重要区别特征。」
如果数据大多是一对多关系(树结构数据)或者记录之间没有关系,那么文档模型是最合适的。但是,如果多对多的关系在数据中很常见呢?关系模型能够处理简单的多对多关系,但是随着数据之间的关联越来越复杂,将数据建模转化为图模型会更加自然。
「图由两种对象组成:顶点(也称为结点或实体)和边(也称为关系或弧)」
。很多数据可以建模为图。典型的例子包括:
-
-
Web图 顶点是网页,边表示与其他页面的HTML链接。
-
公路或铁路网 顶点是交叉路口,边表示他们之间的公路或铁路线。
有很多著名的算法可以在这些图上运行。例如,汽车导航系统搜索道路网中任意两点之间的最短路径,PageRank可以计算Web图上网页的流行度,从而确定搜索排名。