《Apache Drill学习笔记一:环境搭建和简单试用》提到过Apache Drill是受Google的Dremel系统启发而设计实现的,这出于Google公开于2010年的论文“Dremel Interactive Analysis of WebScaleDatasets”。为了弄清楚Apache Drill的运行机制,这篇论文是必定要先仔细研读的,不然就只能像我以前那样仅仅将其做为CSV或者JSON的SQL查询工具使用了,而不能真正发挥其强大的性能优点。数据库
简单说Dremel是Google的“交互式”数据分析系统,能够组建成规模上千的集群,处理PB级别的数据。虽然MapReduce也能够处理这样规模的数据,但它所须要的时间相对比较长,适合数据的批处理,而不适合交互式查询的场景,Dremel正是这样的一个有力补充。数组
Dremel有2个显著特色:ruby
而这正是其余数据库、查询引擎的痛点所在,也正是咱们须要着重了解的地方。微信
Dremel使用的数据就是咱们熟悉的Protocol Buffer格式,但一般状况咱们都是做为序列化方法或者在RPC中传输等场景使用,较少用它来存放大量数据。对于没有接触过Protocol Buffer的读者,能够用JSON类比,两者结构很类似,一个不一样是Protocol Buffer不支持JSON的map(或者说是dict、hashmap)。数据结构
一个Protocol Buffer的Document.proto
文件示例:dom
message Document { required int64 DocId; optional group Links { repeated int64 Backward; repeated int64 Forward; } repeated group Name { repeated group Language { required string Code; optional string Country; } optional string Url; } }
注意的不是数据自己,而是数据的类型,或者说是数据的schema。但从中已经能够看出2个特色:工具
对如此复杂的数据作SQL查询看起来是很让人头疼的,咱们天然想到先简化一下,从最简单的状况考虑。性能
这种数据格式用数学方法严格表示是这样的:学习
t = dom | <A1:t[*|?], ..., An:t[*|?]>
看起来有点复杂,但理解起来很容易。t(原文是希腊字母τ,但为了书写方便这里改为英文字母t)是一个数据类型的定义,而.proto文件就是定义一个或多个数据类型。t有两种可能(|和c语言同样是“或”的意思,一种是基本类型dom(如int、string、float等),另外一种是使用递归方式定义的,即t能够由其余以前定义好的t组成,就像c中的结构体同样,与结构体不大相同的是,每一个包含的t的值能够有多个(*,repeated,相似c中的数组),还能够是可选的(?,optional,以前那个数组能够不包含任何元素)。A1-An是这些t的命名(也就是A1是某个t类型的变量)。其实从这个定义中更容易看出以前总结的2个特色。优化
如今咱们来考虑简单的Protocol Buffer数据,以及如何查询。
这是一个简化的Document.proto
,能够看到它只有一层结构,并且没有repeated
和optional
字段。
message Document { required int64 DocId; string Url; string Country; int64 Code; }
而Document
的数据就是一张普通的二维表:
DocId | Url | Country | Code |
---|---|---|---|
10001 | http://1 | America | 10 |
10002 | http://2 | America | 20 |
10003 | http://3 | China | 30 |
10004 | http://4 | America | 40 |
10005 | http://5 | Japan | 50 |
10006 | http://6 | America | 60 |
... | ... | ... | ... |
能够看出咱们用二维的方式组织数据,但实际是数据在磁盘的地址是一维的,也就是咱们须要按某种方式把它拼接成一维的数据。那最基本的方式有两种:
10001 | http://1 | America | 10 | 10002 | http://2 | America | 20 | ...
-> | -> | -> | -> | -> | -> | -> | -> | -> |
10001 | 10002 | 1003 | ... | http://1 | http://2 | http://3 | ...
-> | -> | -> | -> | -> | -> | -> | -> |
咱们先考虑下对这个表进行select
,如select Url, Code from Document;
若是是按行存的话,每读一个Url
后,都须要跳到下一个Url
的位置,全部要查出的字段都不是连续存放的。并且由于有字符串这样的非定长字段(若是使用定长的预留空间,又会形成大量的空间浪费),不能经过简单计算就能够获得地址,查起来很是痛苦,效率天然不会很高。
而按列存的状况就好不少,只须要找到第一个Url
和第一个Code
的首地址,而后顺序读取到结尾便可。不只实现简单,并且磁盘顺序读取比如随机读取要快,加上更容易优化(好比把临近地址的数据预读到内存,连续的同类型数据更容易压缩存放),效率天然不可同日而语。
那是否是全部状况都须要按列来存数据呢?显然不是。虽然按列读的状况比较多,但写入通常是按行写的,不管是追加、删除、修改,通常都是按行处理的。数据按列存的话,追加时须要把一行数据按字段拆开,分别插入到不一样的地方,删除也是同样,修改更加痛苦。由于若是是相似字符串的不定长字段,按行存的话能够以行为单位预留空间,而按列存的话须要以字段为单位预留空间,或者使用更复杂的方法。想想就要麻烦许多。
数据库每每须要同时照顾到读和写的效率,简单的按行存或者按列存都存在明显的问题(包括下文提到的表join效率等问题),因此每每须要存储复杂的meta数据、添加各种索引、使用各类树型甚至图型结构,来在读和写之间谋得一个平衡点。
而Dremel要轻松一些,由于它被设计成一个查询引擎,即便也有写入功能也不会过多考虑写入的效率,那么显然按列存是合适的。这样即便一张表字段不少,数据量很大,只要记录每一个字段的类型以及对应数据的起始地址等少许信息,查起来就游刃有余。因此若是只是用来查一个巨大的二维表的后,并非很难。
但咱们知道,平时使用的数据很难在一张二维表里表达清楚,每每须要多张表,互相还有关联,查询起来就须要各类join。数据量小还好,数据量一大,join效率直线降低,单表select再快也没用,这才是真正棘手的问题。
Dremel的解决方法不是设法提升join的效率,而是换一种思路,使用嵌套的数据解决简单二维表表达能力太弱的缺点。
再拿出以前的Document.proto
:
message Document { required int64 DocId; optional group Links { repeated int64 Backward; repeated int64 Forward; } repeated group Name { repeated group Language { required string Code; optional string Country; } optional string Url; } }
这样的数据若是用二维表来存放通常须要多张才能描述清楚,处理重复字段也比较痛苦,而一个Protocol Buffer类型就能够描述,但在磁盘的实际存放仍是要动很多脑筋的。
如今就须要搬出论文里的这张图了:
虽然嵌套的数据比以前的二维表更加复杂,仍是有按行存和按列存两种基本方法,并且正如咱们以前提到的,为了查询效率,咱们采用按列存的方法(图中的column-oriented
)。咱们重点关注A、B、C、D、E这些树型关系如何存储。
咱们来准备一些符合Document.proto
的简单的数据:
DocId: 10 Links Forward: 20 Forward: 40 Forward: 60 Name Language Code: 'en-us' Country: 'us' Language Code: 'en' Url: 'http://A' Name Url: 'http://B' Name Language Code: 'en-gb' Country: 'gb'
DocId: 20 Links Backward: 10 Backward: 30 Forward: 80 Name Url: 'http://C'
其中DocId: 10
和DocId: 20
是两个Document
。
Dremel是这样拆解数据的:
能够看出每一个须要存放实际数据的叶子节点都变成了一张二维表,但表中除了字段自身的值。若是是repeated
字段,则在表中增添行;若是是optional
字段,而且数据中不填充,则用NULL
代替(而不是去掉这一行)。但还出现了r
和d
,这两个又是什么东西,并且为什么要记录NULL
呢?
试想若是去掉上图中r
和d
两列,则每一个二维表都变成了一个一维表(list),那么咱们试图把数据还原回去,DocId
没问题,必定是属于两个Document
的。Name.Url
就出现了问题,由于Name
是repeated
的,我怎么知道这3个Name.Url
是全属于第一个Document
,仍是其余状况呢?丢失的信息太多没法还原了。全部咱们须要记录每一个值是不是重复的以及在哪一层重复的(好比是在第一个Name
的第二个Code
,仍是第二个Name
的第一个Code
)。有了这个信息,咱们就能够根据以前的记录一个一个往上拼接来还原原始的数据结构。r
就是作这个的。
r
是重复层次(Repetition Level),记录该列的值是在哪个层次上重。
若是r
是0,则表示是第一个(非重复)的元素,如上图中的DocId
,两个DocId都是第一个元素,比较简单。但其余的字段就比较复杂了,如Name.Language.Code
,一共有五行:
en-us
是第一个Document
(不一样的Document
不算重复,不影响r
和d
的取值,只有repeated
类型的字段才算)里第一个Name
中的第一个Language
里的,重复尚未发生,因此r
是0。en
是第一个Document
里第一个Name
中第二个Language
里的,Language
发生了重复,在/Name/Language层次结构中处于第二层,因此r
是2。en-gb
是第一个Document
里第三个Name
中第一个Language
里的,Name
发生了重复,在/Name/Language层次结构中处于第一层,因此r
是1。NULL
是第一个Document
里第二个Name
中的,Name
发生了重复,在/Name/Language层次结构中处于第一层,因此r
是1。NULL
是第二个Document
里第一个Name
中的,没有发生重复,因此r
是0。这里例子中没有出现多个字段都发生重复的状况,如第二个Name
中的第二个Language
的Code
。若是是这种状况,那么r
取最大的,也就是最近发生重复的字段,这里例子中就是Language
的2。(待验证)
以前还有个问题没有回答,为什么要记录NULL
呢?
若是把图中全部的NULL
都去掉,看会发生什么。 拿Links.Backward
举例,去掉第一行的NULL
后,咱们读到第一个Links.Backward
,必然认为它是属于第一个Document
的,但实际数据中第一个Document
里没有Links.Backward
,彻底搞错了。因此即便是NULL
也必须记录,为了后续的数据知道本身在哪。
那么有了r
后,是否信息就完善了呢?
咱们仍是假设去掉d
的一列,试图还原数据。DocId
依然没问题,Name.Url
也没问题了,直接看Name.Language.Country
吧:
读完第一行咱们获得了:
Document Name Language Country : 'us'
第二行是个NULL
,是在第二层也就是Language
重复的:
Document Name Language Country : 'us' Language Country : NULL
第三行又是个NULL
,是在第一层也就是Name
重复的:
Document Name Language Country : 'us' Language Country : NULL Name Language Country : NULL
第四行是在第一层也就是Name
重复的:
Document Name Language Country : 'us' Language Country : NULL Name Language Country : NULL Name Language Country : 'gb'
看起来彷佛没问题,不过对比原始数据发现第二个Name
不仅没有Country
,连上层的Language
也没有。也就是单看Name.Language.Country
这个表,仍是把数据还原错了。虽然把全部的表都还原出来,而后去掉全部的NULL
以及NULL
上边多余的部分,仍是能够准确还原,但若是只是去查询某个字段,难道须要把其余全部字段所有分析一遍吗?另外没有发生重复的字段,具体是required
、repeated
、仍是optional
的信息也丢了。(此处彷佛还有其余问题)
为了解决这个问题,d
被引入了。
d
是定义层次(Definition Level),记录这个值是在哪一层被定义的。须要注意的是若是这个值是required
的,则层数不包括自身,不然若是是repeated
或optional
的,则包括自身。目的主要是区分是不是required
字段(但如何区分只有一行的repeated
和optional
呢?)。
举例:
Document.Links.Backward
的d
是2(Document
是0)Document.Name.Language.Code
也是2(由于Code
是required
的,因此不包括它本身)对于通常的数据,这个值看起来没什么意义(除了能够区分是不是required
字段),由于已经有值了,从根到它自身整条路径必然是存在的,但对于NULL
则不一样,d
能够说明这个NULL
是在哪一层定义的,也就是解决咱们以前还原Name.Language.Country
数据遇到的问题。
r
和d
这两个值仍是须要好好理解一下,并且还有一些没弄清楚的细节,以及具体查询的复杂逻辑,只能后续继续学习了。
付费解决 Windows、Linux、Shell、C、C++、AHK、Python、JavaScript、Lua 等领域相关问题,灵活订价,欢迎咨询,微信 ly50247。