3G技术应用
首先简单介绍一下Dremel是什么,能解决什么问题。第二部分着重讲Dremel的数据模型,即数据结构。第三部分将谈一下在此数据结构上设计的算法。
Dremel的数据模型起源于分布式系统的应用环境(Protocol Buffers,一种在Google内广泛使用,现已开源的实现)。其数据模型是基于强类型的嵌套记录,抽象语法可以表示成下面公式:
一个例子:
首先来看一下Dremel的数据模型是如何在列式存储下无损的表示出记录的结构的(lossless representation of record structure in a columnar format)。如果仅仅是数值(values)的话,数值本身无法传递出记录(record)的结构信息。我们不知道两个数值是属于两条不同的记录还是在一条记录下,同时我们也不知道一些可选的字段(field)是否显式定义。因此,我们引入了两个概念:Repetition Level和Definition Level。
为了说清楚Dremel模型是如何无损地表示数据的,我想到了两种画法。最终还是决定采用第一种画法,类似有向图,感觉与后面的FSM状态机能更好的对应上。
Repetition Level
Dremel论文中对repetition level的定义听起来比较抽象:at what repeated field in the field's path the value has repeated。意思就是在路径上,在哪个repeated字段上重复了。还是看个例子解释一下吧,以之前的图例中的文档r1中的Code字段为例。
上图清晰地表示出三个Code字段与文档中字段的对应关系。下面来看一下这三个Code的repetition level(简写为r) 0,2,1是如何计算出来的。下图忽略无关的字段,将三个Code字段的完整路径都表示出来。那么就可以简单易懂地看出,r就是这些字段路径上,发生重复了的字段的level。请参考下图中的注释就能很快理解。
大家可能还注意到Name.Code表中除了en-us、en和en-gb三行外,还有两行NULL。第二个NULL是描述文档r2的,我们就分析一下第一个NULL的含义吧。因为文档r1的第二个Name字段下没有Code,而为了说明en-gb是属于第三个Name字段下的,所以在en和en-gb之间加了一行NULL,其r也等于1(Name重复)。同时,由于Code在定义中是required的字段,所以事实上这一行NULL也暗示了:在第二个Name字段下Language也是不存在的。不然Language存在而下面却没有Name,这是不符合文档定义的。
以此类推,其他字段的r值都是这样计算出来的。同时注意一点:我们只保存了有值的字段,如DocId、Name.Url、Name.Language.Code等,而像Links、Name.Language等字段是没必要保存的。
Definition Level
definition level(简写为d)在论文中的定义还比较清楚:Each value of a field with path p , esp. every NULL, has a definition level specifying how many fields in p that could be undefined (because they are optional or repeated) are actually present. 尤其对于NULL来说,路径p上有多少字段可以是不存在(例如在文档定义中是optional或repeated,而不是required),然而实际却存在的。例如文档r1的Links下没有Backward字段,然而Links字段却存在(因为Links下有Forward),所以我们在Links.Backward表中保存一条NULL,并且d=1。对于非NULL字段来说,意义不大,因为d的值对于每种字段来说都是相同的,例如Code都是2,Country都是3。
值得注意的几点是:
Ø 在路径上计算多少字段本可以不存在时,包含了当前字段本身。例如计算Country:us时,Country本身也是optional,也计入总数,所以d=3。
Ø 每种字段只计算1次。例如最下面的Country:gb,在其路径上的3个Name都满足条件,但只计1次,所以d=3,而不是5。(前面提过,也许是我这第一种画法的缘故,需要这一条规则来限定)
数据压缩
前面介绍了数据的保存方法,实际上真正保存时,数据还会被进一步压缩。
Ø 不显式保存NULL,因为它可以通过d来确定:d < 路径上repeated和optional字段总数,就说明是NULL。可以通过前面的例子印证一下。
Ø 总是会被定义的字段的d不会被保存。
Ø r也是仅在必要时才会保存。例如d=0暗示r=0,所以r可以省略不存。
Ø 像DocId这种所有level都是0的,实际上不会保存任何level信息。
Ø 尽可能使用位图。例如假如d最大是3,那么我们只使用2个bit来保存。
2.2 快速编码成列式存储
略,详见论文附录部分的伪代码。
2.3 高效地组装记录
高效地从列式存储数据中组装出记录,对像MapReduce这种面向记录的数据处理工具来说非常重要。我们的目标是:给定字段的子集,我们能重新构建出仅包含选中字段的原始记录,而过滤掉其他字段。核心思想是:使用有限状态机(finite state machine, FSM)读取每个字段的值和level,顺序地追加到输出流中。FSM为每种字段都关联一个field reader。状态转变通过repetition level来标记。一旦reader抓取到值,我们继续看下一repetition level来决定使用哪个reader。FSM就这样从开始状态到结束状态遍历完每条记录。
下面还是用前面的例子,通过DocId和Name.Language.Country这两个字段的重建,来详细解析一下FSM的工作过程。关键步骤用红色加粗标记。
1. FSM委托Reader1读取DocId第一行,通过r=0重建记录。
2. 检查DocId第二行,发现r=0,则Reader1停在当前“游标”位置。FSM将状态变化到Name.Language.Country。
3. FSM委托Reader2读取Name.Language.Country第一行,通过r=0重建记录。
4. FSM委托Reader2读取Name.Language.Country第二行。通过r=2(说明Language字段重复,即Language有多个)重建记录。
5. FSM委托Reader2读取Name.Language.Country第三行。通过r=1和d=1(说明只有Name字段不是NULL)重建记录。
6. 略过第四行。
7. 检查到第五行,发现r=0,Reader2停在当前位置。FSM再次发生状态变化,继续重建文档2的记录。
8. FSM委托Reader1继续读取DocId第二行(之前Reader1就停在这里了)。
9. 到这里应该已经很清楚了,最后过程就略说了:DocId中没有数据了,FSM状态变化,Reader2继续读取Country的最后一行数据,重建出记录。
注:论文原图中少了第二个Name字段,我觉得应该加上吧。在第五步被重新构建出来。为什么在原图中没有呢?
前面例子的完整FSM就是这样的:
“神秘”的r和d
单从数据结构来看的话,我们可以这样解释r和d的含义。r代表着当前字段与前一字段的关系,是在哪一层合并的,即公共的父结点在哪?举例来说,假如我们重建到了Code='en',通过r=2可以知道是在Language那一层发生了重复。
为了保持原纪录的结构,我们会保存一些NULL数据,而d就是用于重建NULL字段。通过d的值,就能知道NULL的结构。例如下图,通过r=1知道应该合并到Name那一层。而通过d=1则知道路径上只有一个字段,即不仅仅是Code字段不存在,Language也不存在。这样就把NULL正确地重建出来了,那么接下来的Code='en-gb'的层级也就不会乱了。
然而这只是从静态的数据结构来解释,而r和d的深层次含义还是要看FSM是如何执行的。真正的因果关系是FSM的执行方式决定了数据结构的设计。
3 记录查询3.1 从FSM角度看r和d
先看一下前面例子的完整FSM的样子。如果把Protocol Buffer中对数据格式定义的schema看作是编译原理中的语法定义的话,那么一般可以使用工具如antlr, yacc自动生成自动机,手写的话是相当恐怖的吧。
对列数据的完整遍历就是这个样子的:
在讨论查询如何执行之前,先继续刚才未完成的题目,r和d的本质,这次通过动态的FSM的角度来分析,而不是静态的数据结构了:
Ø FSM状态机只是定义了状态的变更,即处理流程应当如何在各个列的存储表之间跳转,而实际数据还是在表中保存。有点像数据库索引,遍历时是根据FSM进行跳转,然后对某一列的表进行table scan。但索引是靠字段值的顺序组织,因为数据库表之间没什么嵌套关系,而Dremel的FSM则是靠字段之间的嵌套关系来组织。
Ø 状态机中线条上的数字表示什么?回忆一下,数字表示的是:字段的数据表中当前行的下一行的r值。通过检测下一个r值来决定跳转。因为r=0,则说明下一行与当前行所表示的字段一定不在同一路径,否则必然会在某一Level上有共同的字段(路径的部分重叠)。注意这是由于Protocol Buffer的schema不是树,没有共同的根所导致,否则所有字段必然都会在根重复,上面对r的解释也就没意义了。以repeated的Forward为例,检查到下一行r=1说明40、60都是接在20字段下面的。Code字段也是同样道理。
Ø Name.Language.Code到Name.Language.Country之间的线上为什么是0,1,2?因为Name.Language.Code是required不是repeated,读取后不管下一行的r值是多少都要去读Name.Language.Country。同理Name.Language.Country也是读完不管怎样都跳到下一字段。
Ø 最复杂的要属Name.Url了,因为它是schema里定义的最后一个字段。在Name.Url这要决定到底是继续下一文档如r2的处理,还是跳回到本文档的其他字段继续处理。具体分析一下:r=0说明当前文档中没有Name字段了。为什么这么说?因为如果文档后面真有Name字段,假如下面有Url,则当前表中的下一条应该是r=1;假如下面没有Url,则当前表的下一条应该是r=0的NULL。这里NULL又发挥用处了!所以中间部分的NULL能保持结构无损,而后面部分的NULL能提示文档是否结束。
3.2 查询引擎
至此,我们已经彻底摸清Dremel数据模型以及FSM的基本运行方式了。现在终于可以分析Dremel是如何解析和执行类SQL查询的了。查询语言类似SQL,输出也是个嵌套式的记录,以及schema定义。
那么查询引擎如何执行呢?首先为查询语句中涉及到的每个字段都打开一个Reader来读取数据,然后就是根据WHERE中的条件过滤以及根据SELECT中的条件投影并聚合了。难点在于:重建出层次关系,再进行过滤和聚合。例如,过滤掉DocId=20很容易,但其实文档r2的所有记录都应被过滤。因为WHERE中两个条件是AND关系,同时DocId又是最底层的字段,所以相当于r2这一整棵树都被裁剪掉了。Code=en-gb也是由于所在的Name字段下没有满足http开头的Url字段,而被间接的过滤掉了。
聚合也是同样道理,有了层次关系,才能正确的聚合。例如Code=en-us,en和Url=http://A是同一个Name下的,COUNT和字符串拼接时会一起处理。而Url=http://B则是另一个Name下的,要分开处理。
全部0条评论
快来发表一下你的评论吧 !