Designing Data-Intensive Application 数据模型与查询语言

Designing Data-Intensive Application 数据模型与查询语言

数据建模是开发里面最重要的部分

关系模型和文档模型

最著名的数据模型是SQL,数据被组织成relations,在SQL里称为table,每个关系都是tuple的无序集合(SQL里称为row)。

关系型数据库的核心在于商业数据处理,特别支持事务处理(比如银行交易、酒店订房、采购...)和批处理(处理会计数据)。

关系模型的目标是将实现细节隐藏在间接的API之后。还有其他模型也出现过,比如网络、层次数据模型。

NoSQL, Not Only SQL

为什么要使用NoSQL数据库?优点和需求驱动如下:

  1. 比关系型数据库扩展性更好,适应超大数据集或超高写入吞吐量
  2. NoSQL大多免费开源
  3. NoSQL能够支持一些关系型数据库不能支持的特定查询
  4. 关系型数据库的限制性——数据模型的动态和表达力不够

好处:消除对象-关系不匹配

什么是“关系-对象不匹配”

SQL数据存储在表中,往往和应用层代码之间有个中间层来转换,比较笨重。一般使用ORM框架来解决,但是完全隐藏两个模型之间的差异还是很困难。这种笨重和不变就像给电池和电灯之间的电线加大了电阻值,这就叫关系-对象不匹配

一份简历里面包含很多字段,教育信息、基本信息、工作经历....,如果关系型数据库存储这些数据,需要简历大量的表来存储并使用user_id来关联,但是在文档型数据库里面,这些数据就可以转化为json格式、XML格式的文档型数据,当应用层需要这些数据的时候,则交由代码自由的处理。

消除之后有什么好处?

优点是:相比于关系型数据库拿着各种id,比如user_id、school_id、addr_id去各种表查询数据,文档型可以很快的将想要的一份丰富的数据拿出来。这种优点称为文档型数据库的局部性。并且大部分时候,这种类型的数据几乎就很接近应用程序的数据结构了。

缺点:面对多对一/一对多的窘境

有的时候,使用文档数据库存储的json字符串里面部分字段,xx_id也会存储id类型的数据。思考一下,为什么不存储字符串呢?比如存储user_name,难道不会比存储user_id来的更加爽快和直白吗?
当然不能如上句话那样认为,使用id的好处在于:

  1. 对变更的支持更好,id数字是没有直接意义的,但是其标识的背后的信息可以改变。
  2. 避免重复,id是引用,背后标识的信息只用存储一份即可。如果使用字符串,就需要考虑将每一处重复的副本进行更新,写入的负担极大。

但是对于这种一对多或者多对一的关系来说,文档型数据库并不是其理想型。 因为大部分文档型数据库不像关系型数据库,没有主键、也不能join关键字关联查询。

其他的几种数据模型

层次模型

层次模型描述一份简历

层次模型最早起源于68年首次商业发布的IBM信息管理系统,特点是将数据表示为“记录当中的记录”。机器类似JSON结构。

虽然树状结构能够很好的支持1-n,但是n-n的数据关系还是需要开发人员“反规范化”(数据库里的规范化概念)的去

  • 复制多份数据
  • 手动解析记录之间的引用

网络模型(CODASYL模型)

类似上面树形结构,对于网络结构来说,一个节点可能存在多个父节点,这样的组织形式就生成了网状数据结构。有一个称为数据系统语言会议CODASYL的委员会对其设立标准,并且有多个数据库厂商对此标准各自实现。网络模型也称为CODASYL模型。

如何访问网状结构组织成的庞大记录网呢?

  1. 记录之间的衔接类似指针
  2. 开发人员需要记住指针的遍历路径,“选择访问路径”是一件很重要的事情

这种结构的缺点非常明显,就是遍历速度很慢。查询和更新都非常复杂且没有灵活性。

关系模型的好,谁知道?

关系模型简单的定义了数据的格式,表=行的集合,不需要复杂的类似JSON、层次模型、网络模型的嵌套结构或者是复杂的对访问遍历路径的记忆。好处如下:

  1. 只要指定某字段为key即可任意在表里插入行,不需要考虑键会不会和其他表里的某个键冲突
  2. 查询优化器可以优化执行查询的顺序,决定使用何种索引,等价于一种“选择访问路径”的动作。但是这个动作不需要开发者关心
  3. 对于索引的变动是很方便的,同一查询语句,不同的索引加持能够得到不同的查询效果。不改动查询语句,即可使用新的索引——这个特点使得应用发生变化变得容易。

比较

  1. 文档数据库是特殊的一种层次模型,即嵌套保存记录,比如一个用户的JSON信息里面既有教育、工作经验信息,还有地址、爱好等信息,这些信息都存储在一条记录里面。而不是分散为多个记录保存在多个表里面。用嵌套保存记录来实现1-n的关系。
  2. n-1和n-n的 关系的时候,文档型数据库存储的JSON字符串和关系型数据库就没有了区别,基本上都是通过“保存唯一标识符+联结操作”来解析。

数据查询语言

浅尝声明式查询语言和命令式查询语言的区别

数据查询语言是跟随着关系模型的发展而引入的,相比起IBM发展的IMS和前面提及的网络模型(CODASYL)是属于命令式的查询数据方法,SQL是声明式的。

命令式的查询语言:

function getSharks () {
  var sharks = [];
  for ( var i = 0 ; i < animals.length; i++) {
      if (animals[i].family === "Sharks" ) {
          sharks.push(animals[i]);
      }
}

或者下面这种关系代数也是属于命令式语言的一种。

\[sharks=\sigma_{family}=_{"sharks"}(animal) \]

上面所给的命令式语言的特点有什么?

  1. 支持逐行遍历代码
  2. 评估条件
  3. 更新遍历
  4. 决定循环的次数

那么如何在“一群动物里面找到鲨鱼”用说明式查询语言如何去实现呢?就正如下面这样一个简单的SQL语句。

SELECT * FROM animals WHERE family ='Sharks';

回顾一下,这种声明式语言去查询数据,我们仅仅需要做如下的事情即可:

  • 指定所需数据的模式 animals
  • 结果必须符合哪些条件 family ='Sharks'
  • 如何将数据转换(例如:排序,分组和集合)

DBMS向我们隐藏了诸如 使用哪些索引和哪些连接方法,以及以何种顺序执行查询的各个部分 的这些事情。所以这就是声明式比命令式查询语言更迷人的地方。——无需对查询做任何更改的情况下进行性能提升

再比如。如何在数据库查询的过程中,回收查询过程中已经不在读取的磁盘空间,这种回收的行为一般就会影响我们正常的读取,对于命令式就是一个比较棘手的问题,即 如何确定查询的顺序。

对于SQL来说,SQL从不指定查询的顺序。比如我先从行数为1的地方开始查起,直到查询到行数为100的时候终止。这件事对于一个命令式的查询语句来说,就可能会要求,这样一个1~100的数据容器最好保证一定的顺序,并且中途不要有插入或者删除。就好像阿里巴巴在java规约里面不要在for循环遍历里面删除list的元素一样。

但是对于SQL来说,就没有这种限制。

最后,SQL是完全支持并行执行的语言。现在通过增加CPU的内核的数量来加快速度,而不是以往的提高CPU的时钟速度来实现这个目的。命令式的代码想要在多内核/机器上面并行执行是不容易的,因为指令必须按照特定的顺序执行。但是SQL语言不一样,SQL语言会规定/描述想要的结果是来自哪儿,有什么条件限制,但是不会指定实现这个查询目的需要什么算法。DBMS会自由使用查询语言的并行实现。

以浏览器上的查询为例比较声明式/命令式

假设现在有一个介绍鲨鱼的网站,其中一部分html代码如下:

<ul> 
    <li class= "selected" > 
        <p> 鲨鱼 </p> 
        <ul> 
            <li> 大白鲨</li> 
            <li> 虎鲨 </li> 
            <li> 锤头鲨 </li> 
        </ul> 
    </li> 
    <li>
      <p> 鲸鱼 </p> 
        <ul> 
            <li>蓝鲸 </li> 
            <li> 座头鲸</li> 
            <li> 长须鲸</li> 
        </ul> 
    </li> 
</ul> 

使用声明式查询语言,将当前所选的页面(li标签)的背景编程蓝色。

一般CSS语言是如下实现的:

 li.selected > p {
    background-color: blue;
}

和上面回顾SQL的使用方式一样,使用CSS这种命令式的查询语句,我们做的事情就如上面代码一样,选择了一种,也就是我们想要将“背景变蓝”这件事作用在什么地方的选择。使用另外一种声明式语言XSL(CSS 和XSL的共同之处在于,它们都是用于指定文档样式的声明式语言)的示例如下:

 <xsl:template match="li[@class='selected']/p">
    <fo:block background-color="blue">
        <xsl:apply-templates/>
    </fo:block>
</xsl:template>

表达式 li[@class='selected']/p一样指明了一个模式。

下面给出一个命令式的查询语言的案例,比如JavaScript,用JavaScript去操作实现这个背景变蓝的功能。

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");
			}
		}
	}
}

一个比较明显的槽点是,相较于上面声明式查询语言:命令式查询语言明显更长,可读性比较差。

另外几个不明显的槽点介绍如下:

  1. 如果用户选到其他的li上,那么现有的蓝色的标签不会被移除,而css则不一样,借助浏览器可以自动检测当条件不适用的时候将蓝色背景取消。
  2. 如果不使用liElements[i].className方式去判断,而是使用新的API区获取属性,比如 document.getElementsBy ClassName("selected") 那么随之而来的,代码一定需要重写。
  3. 浏览器会保证兼容性的同时优化CSS的表现性能,但是JavaScript很难做到。

MapReduce初体验

什么是MapReduce

MapReduce是一种编程模型,作用是用来在多机器上处理大规模数据。诸如NoSQL领域的MongoDB就支持了有限形式的MapReduce,能够在多个文档(JSON)中执行只读查询。

和前面数据查询语言章节介绍的命令式查询语言和声明式查询API不同,MapReduce介于两者之间,既有命令式查询语言的代码(代码片段),也有声明式查询API当中比较语义化的东西。

在代码片段中描述此次查询的逻辑,这个代码片段重复的被调用。正如java 8的Stream流里面调用map一样,里面描述的是function施加在流元素上的逻辑。代码片段里面描述此次查询的逻辑一般基于map和reduce函数(也叫collect和fold/inject函数)

这两个函数频繁出现各种函数式编程语言当中,正如举例的Java 8。

举例说明:统计每月观察到的鲨鱼数量

假设你是⼀名海海洋⽣生物学家,每当你看到海洋中的动物时,你都会在数据库中添加⼀条观察记录。现在你想⽣生成⼀个报告,说明你每月看到多少鲨鱼。

如果是SQL的声明式查询API,实现可能如下:

 SELECT
    date_trunc('month', observation_timestamp) AS observation_month,
sum(num_animals) AS total_animals
FROM observations WHERE family = 'Sharks' GROUP BY observation_month;

按照观察记录的observation_month分组之后,对这个月观察到的动物进行计数并最后排序给出从大到小的顺序的记录。

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"
    });

分成三部分去解析这个模型:

首先第一个map函数里面最主要的就是最后的emit(单词意味:发射),函数的作用是,将年月拆分之后组成一个类似"1998_09"的key值。并将动物的数量numAnimals作为val,这个键值对会被emit给“发”出去。

齐次,一旦emit发出去这个键值对之后,reduce函数会接管,并将相同key的元素的val进行一次Array.sum的操作。也就是对所有相同key的val进行求和。

最后介绍一下query对象,这个对象指定了查询的条件是family="Sharks",并制定了最后的结果写入的指定的report。

一般上面这个MapReduce编程模型的入参如下两个JSON文档:

 {
      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
}

上面两个case,分别能够产生出两次emit的触发,emit("1995-12",3) 和emit("1995-12",4),reduce的结果就是7。

两个必须知道的知识点

map和reduce函数在功能上有所限制,它们必须是纯函数

什么是纯函数?

  1. 只使⽤用传递给它们的数据作为输⼊,不会去额外的地方获取数据,比如在函数内调用一次数据库
  2. 没有副作用,比如不会在函数过程中修改一个引用

既然纯函数有如此多的限制,那么纯函数带来什么好处

对一个数据集使用MapReduce编程模型,纯函数能够支持 操作的顺序不限制。比如

  • 先排序再过滤数据
  • 先过滤在排序

这两种顺序对产生的结果没有影响。

纯函数还能够支持操作失败重试。

MapReduce编程模型的一个重要特性就是:支持在查询当中嵌入诸如JavaScript类的命令式代码。但是必须要注意的是,嵌入的两个JavaScript函数必须要密切合作。Java 8当中的reduce的其中一个重写就是如此,accumulate方法和combiner就是如此密切合作的。

图数据模型

之前说过,1对n的数据关系适合树状结构化数据,但是遇到n对n的数据关系,除了关系模型,还有图数据模型可以适用起来很好的表达。比如社交图谱、网络、公路铁路网都可以用图数据模型来表达。

图由两种对象组成:顶点、弧。

属性图模型

属性图模型是第一种介绍的数据模型:

  1. 每个顶点包括其唯一标识符、出边、入边和几个属性(key-val键值对形式)。
  2. 每个边包括,唯一标识符、边的头尾顶点、头尾顶点之间的关系描述和几个属性(key-val键值对形式)。

这种模型用SQL很好去解决,给出外键即可,通过一个顶点+其外键可以访问所有的顶点,数据结构非常清晰。并且能够支持各种的数据粒度(只要体现在k-v键值对里即可)。

其他对属性图的查询语言比如Cypher

其实Cypher语言的可读性还是比较高的,比如下面这个语句就表达了爱达荷州是属于USA的。

(Idaho) - [:WITHIN] ->(USA)

通常对于声明式查询语⾔言来说,在编写查询语句时,不不需要指定执行细节:查询优化程序会自动选择预测效率最⾼的策略略,因此你可以继续编写应⽤用程序的其他部分。

再比如最熟悉的SQL

对于SQL来说,执行图数据的查询比较困难,n-n的数据模型需要我们实现知道每个顶点几个边,但是这往往都是动态变化的。

执行图数据的查询常见的就是不停的使用类似union、inner join这类关键字帮助我们查询数据。

使用三元组存储和SPARQL

什么是三元组

三元组存储也叫三元组存储模式,大体上与属性图模型相同,用不同的词来描述相同的想法。

在三元组存储中,所有信息都以非常简单的三部分表示形式存储(主语,谓语,宾语)。例 如:

  1. 三元组(吉姆, 喜欢 ,香蕉)中,吉姆是主语,喜欢是谓语(动词),香蕉是对象。
  2. lucy的年龄是33,用三元组表达其主谓宾就是(lucy, age, 33)

以上面第二个例子为例, (lucy, age, 33) 就像属性 {“age”:33} 的顶点“lucy”。其中:

  1. 主语是其尾部顶点 lucy
  2. 谓语是图中的一条边 age
  3. 宾语是其头部顶点 33

比如下面这张图使用三元组的称为“Turtle”的数据格式,其中一部分内容表达如下:

image-20220129214818218

turtle语言

@prefix : <urn:example:>.
 _:lucy     a       :Person.
 _:lucy     :name   "Lucy".
 _:lucy     :bornIn _:idaho.
 _:idaho    a       :Location.
 _:idaho    :name   "Idaho".
 _:idaho    :type   "state".
...//省略

_:lucy表示一个叫做lucy的顶点,第2、3、4行都以lucy打头意味着,从lucy开始引用了三个顶点。

RDF数据模型

先讲一个语义网络的知识点

网站已经将信息发布为文字和图片供人类阅读,为什么不将信息作为机器可读的数据也发布给计算机呢?

资源描述框架(RDF)的目的是:作为不同网站以一致的格式发布数据的一种机制,允许来自不同网站的数据自动合 并成一个数据网络——一种互联网范围内的“关于一切的数据库“

理想很美好,但是语义网并没有实现出来。

为什么要在讲RDF之前说一下语义网络呢 ? 因为之前讲述了三元组存储,很多人认为三元组存储数据模型和语义网络紧密相连,比如一种叫做Datomic的NoSQL数据库,就是使用类似三元组的数据存储模型去存储数据。

RDF数据模型案例

下面就是一个使用RDF语法去表示上面用turtle语言去描述的关于"爱达荷州属于USA并且USA属于北美洲大陆"这样一个信息。

<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#lives_in> 或者是 <http://my-company.com/namespace#within>

这样做的目的是为了能够将一部分数据和另外一部分数据结合起来,赋予单词 within 或者 lives_in 不同的含义。http://my-company.com/namespace不一定能解析出来什么东西,只是作为一个命名空间。

SPARQL查询语言

SPARQL(发音为“sparkle”)查询语言是为了查询RDF数据模型的查询语言。比如下面的例子(查找生在USA,生活在欧洲的人的名字),使用SPARQL查询的话就是。

PREFIX : <urn:example:>
SELECT ?personName WHERE {
  ?person :name ?personName.
  ?person :bornIn  / :within* / :name "United States".
  ?person :livesIn / :within* / :name "Europe".
}

"?person :bornIn / :within* / :name "United States"." 和之前提及的Cypher语言类似:

(person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (United States)   # Cypher

Datalog:更古老的语言

Datalog是比SPARQL或Cypher更古老的语言,被用于少数的数据系统。数据模型类似于三元组模式,但进行了一点泛化。把三元组写成谓语(主语,宾 语),而不是写三元语(主语,宾语,宾语),下面是使用Datalog格式表示的一些数据。

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).

Datalog是Prolog的一个子集,会比较相似。那么是如何去查询呢?如下:

within_recursive(Location, Name) :- name(Location, Name). /* 规则 1 */
within_recursive(Location, Name) :- within(Location, Via), /* 规则 2 */

within_recursive(Via, Name).migrated(Name, BornIn, LivingIn) :- name(Person, Name), /* 规则 3 */
born_in(Person, BornLoc),
within_recursive(BornLoc, BornIn),
lives_in(Person, LivingLoc),

within_recursive(LivingLoc, LivingIn).?- migrated(Who, 'United States', 'Europe'). /* Who = 'Lucy'. */

和Cypher和SPARQL不同的是,Cypher和SPARQL使用SELECT立即跳转,但是Datalog需要将新的规则告诉数据库。_recursive 和 migrated 。

在规则中,以大写字母开头的单词是变量,比如Name, BornIn, LivingIn(名称、出生地、和居住地)。举个例子:name(Location, Name)就是将Location = namerica 和 Name ='North America' 绑定并且可以匹配三元组 name(namerica, 'North America') 。

:- 操作符能够在操作符的右侧,找到位于的匹配,理解为,规则作用的时候,:- 操作符左侧能够将变量替换为它们匹配的值。

下面举个例子:

within_recursive(Location, Name) :- name(Location, Name). /* Rule 1 */
within_recursive(Location, Name) :- within(Location, Via), within_recursive(Via, Name). /* Rule 2 */

对于这两句来说:

  1. 一旦数据库当中存在name(namerica, 'North America') ,我们可以匹配规则1:生成 within_recursive(namerica, 'North America') ,

    因为:within_recursive(Location, Name)

    • 'North America'赋值给Name变量
    • namerica赋值给Location变量

    所以生成的是within_recursive(namerica, 'North America')

  2. 一旦数据库当中存在

    1. within(usa, namerica),即Location=usa, Via=namerica
    2. 再加上**上一步生成的生成 within_recursive(namerica, 'North America') **

    我们可以匹配规则2,递归生成within_recursive(Location, Name) 为within_recursive(usa, 'North America')。

  3. 规则3的匹配其实也仿照上面的规则2的匹配,根据:-后面的信息进行谓语的匹配。

总结

上面讲了些什么?

介绍了一些数据模型,从开始的层次数据模型的好处(类似JSON文档的数据展示结构,很好的支持1-n)开始介绍,到发现其缺点(多对多关系表达不变)结束。申引出关系模型的出现,关系模型尤为支持商业应用开发,其API式的设计,对开发屏蔽了很多需要操心的事情,比如主键、索引,查询SQL的顺序(优化器做的事情)。

但是近来处于经济性和关系型数据模型的表达能力等考虑,出现了很多适应特定场景的数据存储方式 —— NoSQL。NoSQL数据存储包括两个方向:

  1. 文档数据库, 比如存储一个JSON格式的k-v数据库,文档之间的关联不像图形数据库那样多。
  2. 图形数据库,存储图数据模型。数据的特点是,某一个数据节点可能和多个数据节点发生关联,比如某个Person,既可能是另外一个Person的雇员,又可能是某个Person的父亲。

三种数据模型至今都广泛使用,关系、文档、图形。各自的查询语言和框架随之产生,比如文章前面说的SQL,MapReduce、MongoDB的聚合管道(既有命令式又有声明式风格)、Cypher,SPARQL和Datalog,或者是类似声明式查询语言的CSS和XSL/XPath。

另外,还可以捎带着谈一个没有述说的数据模型,也就是全文搜索,全文搜索这个数据模型也会伴随着数据库一起使用。之后会介绍到

读完这章,应该需要掌握什么?

  • 知道哪几种数据模型?各自的特点是什么
  • 这些数据模型的查询语言分为哪几类?各自特点有什么?举个例子?
  • MapReduce是什么?
  • MapReduce编程模型的特点是什么,这种编程模型带来的好处和限制分别是什么?
  • 什么是纯函数,MapReduce里面的两个函数的特点是什么?举个例子?
  • 简要介绍一下图数据模型?什么是RDF?
posted @ 2022-02-04 17:39  來福l4ifu  阅读(66)  评论(0编辑  收藏  举报