Apache Parquet是Hadoop生态圈中一种新型列式存储格式,它能够兼容Hadoop生态圈中大多数计算框架(Mapreduce、Spark等),被多种查询引擎支持(Hive、Impala、Drill等),而且它是语言和平台无关的。Parquet最初是由Twitter和Cloudera合做开发完成并开源,2015年5月从Apache的孵化器里毕业成为Apache顶级项目。html
Parquet最初的灵感来自Google于2010年发表的Dremel论文,文中介绍了一种支持嵌套结构的存储格式,而且使用了列式存储的方式提高查询性能。java
论文是英文的,学习起来有点难度,幸亏找到了一篇翻译的文章,对比着看很好。git
此处将该文章复制过来。github
地址: http://lastorder.me/dremel-make-simple-with-parquet.html数据库
原文:Dremel made simple with Parquet | Twitter Engineering Blogapache
Google 对于传说中3秒查询 1 PB 数据的 Dremel,有一篇论文:Dremel: Interactive Analysis of Web-Scale Datasets http://research.google.com/pubs/pub36632.html. 这篇论文基本上在描述 Dremel 的数据存储格式.编程
用容易理解但不许确的的话归纳上面那篇论文,就是怎么把一些嵌套的 Protobuff 结构(有相同 schema,若是你不熟悉 Protobuff,那类比 xml 或者 json),拆成若干个表存储(就是逻辑上的二维表),而后经过查那些表,还能快速拼装回原来的 PB(指 Protobuff 下同),再并且,若是你只关注嵌套结构中的某一个层级的某一部分,我能够只读那一部分的数据,只把你关心的那一部分拼装回来,所谓指哪打哪,因为不用读其余没必要要的部分,因此省掉了不少 IO,因此速度很快. 然而因为我很笨,因此一直感受看的云里雾里,直到 2013年9月11号,Twitter 的 Engineering blog 发了一篇博客叫 Dremel made simple with Parquet,看事后恍然大悟. 如下就翻译这篇博客,算是对本身阅读的总结,也与更多人分享.json
对于优化『关系型数据库上的分析任务』,列式存储(Columnar Storage)是个比较流行的技术. 这一技术对处理大数据集的好处是有据可查的,能够参见诸多学术资料,以及一些用做分析的商业数据库.(http://people.csail.mit.edu/tdanford/6830papers/stonebraker-cstore.pdf, http://www.vldb.org/pvldb/,http://www.monetdb.org/)数据结构
咱们的目标是,对于一个查询,尽可能只读取对这个查询有用的数据,以此来让磁盘 IO 最小. 用 Parquet,咱们作到了把 Twitter 的大数据集上的 IO 缩减到原来的 1/3. 咱们也作到了『指哪打哪』,也就是遍历(scan)一个数据集的时候,若是只读取部分列,那么读取时间也相应会缩短,时间缩短的比例就是那几列的数据量占所有列数据量的比例. 原理很简单,就是不采用传统的按行存储,而是连续存储一列的数据. 若是数据是扁平的(好比二维表形式),那列改为按列存储毫无难度,处理嵌套的数据结构才是真正的挑战.框架
咱们的开源项目 Parquet 是 Hadoop 上的一种支持列式存储文件格式,起初只是 Twitter 和 Coudera 在合做开发,发展到如今已经有包括 Criteo公司 在内的许多其余贡献者了. Parquet 用 Dremel 的论文中描述的方式,把嵌套结构存储成扁平格式. 因为受益于这种技术,咱们决定写篇更通俗易懂的文章来向你们介绍它. 首先讲一下嵌套数据结构的通常模型,而后会解释为何这个模型能够被一坨扁平的列(columns)所描述,最后讨论为何列式是高效的.
何谓列式存储?看下面的例子,这就是三个列 A B C.
若是把它换成行式存储的,那么数据就是一行挨着一行存储的
按列存,有几个好处:
首先是嵌套结构的模型,此处选取的模型就跟 PB 相似. 多个 field 能够造成一个 group,一个 field 能够重复出现(叫作 repeated field),这样就简单地描述了嵌套和重复,没有必要用更复杂的结构如 Map / List / Sets,由于这些都能用 group 和 repeated field 的各类组合来描述. (熟悉 PB 的人,对这里说的东西应该很清楚,由于这就是跟 PB 同样的,若是此处有疑惑,最好的方法是当即左转出门去看一下 PB)
整个结构是从最外层一个 message 开始的. 每一个 field 有三个属性:repetition、type、name. 一个 field 的 type 属性,要么是 group,要么是基本类型(int, float, boolean, string),repetition 属性,有如下三种:
例如,下边是一个 address book 的 schema.
message AddressBook {
required string owner;
repeated string ownerPhoneNumbers;
repeated group contacts {
required string name;
optional string phoneNumber;
}
}
Lists(或者 Sets)能够用 repeated field 表示.
Maps,首先有一个 repeated field 在外面,里面每一个 field,是一个 group,group 里面是 key-value 对,其中key 是 required 的.
列式存储,简单来讲就是三件事:1. 把一个嵌套的结构,映射为若干列 2. 把一条嵌套的数据,写入这些列里. 3. 还能根据这些列,把原来的嵌套结构拼出来. 作到这三点,目的就达到了.
译注:直观来看,嵌套结构含有两种信息:1. 字段的嵌套关系 2. 最终每一个字段的值. 因此如何转换成列式也能够从这里下手,分别解决『值』和『嵌套关系』.
Parquet 的作法是,为嵌套结构的 schema 中每一个基本类型的 field,创建一个列. 若用一棵树描述schema,基本类型的 field,就是树的叶子.
上边的 address book 结构用树表示:
观察上图,其实最终的值,都是在基本类型的 field 中的,group 类型的 field 自己不含有值,是基本类型组合起来的.
对上图蓝色叶子节点,每一个对应一个列,就能够把结构中全部的值存起来了,以下表.
如今,『值』的问题解决了,还剩『嵌套关系』,这种关系,用叫作 repetition level 和 definition level 的两个值描述. 有了这俩值,就能够把原来的嵌套结构彻底还原出来,下文将详细讲解这两个值究竟是什么. ]
( 这俩 Level 容易把人看糊涂,若是看文字描述没明白,请看例子回头再看文字描述)
为支持嵌套结构,咱们须要知道一个 field,到哪一层,变成 null 了(就是指field没有定义),这就是 definition level 的功能. 设想,若是一个field 有定义,则它的parents 也确定有定义,这是很显然的. 若是一个 field 是没有定义的,那有可能它的上级是没定义的,但上上级有定义;也有多是它的上级 和 上上级都没定义,因此须要知道究竟是从哪一级开始没定义的,这是还原整条记录所必须知道的.
译注:(假设有一种一旦出现就每代必须遗传的病)若是你得了这个病,那么有可能你是第一个,你爸爸没这个病; 也多是从你爸爸开始才出现这种病的(你爷爷还没这种病); 也有多是从你爷爷开始就已经得病了. 反过来,若是你爸爸没这个病,那么你爷爷确定也是健康的. 你须要一个值,描述是从你家第几代开始得病的,这个值就相似 definition level. 但愿这比喻有助于理解.
对于扁平结构(就是没有任何嵌套),optional field 能够用一个 bit 来表示是否有定义: 有:1, 无:0 .
对于嵌套结构,咱们能够给每一级的 optional field 都加一个 bit 来记录是否有定义,但其实没有必要,由于如上一段所说,由于嵌套的特性上层没定义,那下层固然也是没定义的,因此只要知道从哪一级开始没定义就能够了.
最后,required field 由于老是有定义的,因此不须要 definition level.
仍是看例子,下边是一个简单的嵌套的schema:
message ExampleDefinitionLevel {
optional group a {
optional group b {
optional string c;
}
}
}
转换成列式,它只有一列 a.b.c,全部 field 都是 optional 的,均可能是 null. 若是 c 有定义,那么 a b 做为它的上层,也将是有定义的. 当 c 是 null 时候,多是由于它的某一级 parent 为 null 才致使 c 是 null 的,这时为了记录嵌套结构的情况,咱们就须要保存最早出现 null 的那一层的深度了. 一共三个嵌套的 optional field,因此最大 definition level 是 3.
如下是各类情形下,a.b.c 的 definiton level:
这里 definition level 不会大于3,等于 3 的时候,表示 c 有定义; 等于 0,1,2 的时候,指明了 null 出现的层级.
required 老是有定义的,因此不须要 definition level. 下面把 b 改为 required,看看状况如何.
message ExampleDefinitionLevel {
optional group a {
required group b {
optional string c;
}
}
}
如今最大的 definition level 是 2,由于 b 不须要 definition level. 下面是各类情形下,a.b.c 的 definition level:
不要让 definition level 太大,这很重要,目标是所用的比特越少越好(后面会说)
对于一个带 repeated field 的结构,转成列式表示后,一列可能有多个值,这些值的一部分是一坨里的,另外一部分多是另外一坨里的,但一条记录的所有列都放在一列里,傻傻分不清楚,因此须要一个值来区分怎么分红不一样的坨. 这个值就是 repetition level:对于列中的一个值,它告诉我这个值,是在哪一个层级上,发生重复的. 这句话不太好理解,仍是看例子吧.
这个结构转成列式的,实际也只有一列: level1.level2,这一列的各个值,对应的 repeatiton level 以下:
为了表述方便,称在一个嵌套结构里,一个 repeated field 连续出现的一组值为一个 List(只是为了描述方便),好比 a,b,c 是一个 level2 List, d,e,f,g 是一个level2 List,h 是一个level2 List,i,j 是一个level2 List。a,b,c,d,e,f,g 所在的两个 level2 list 是同一个 level1 List 里的,h,i,j 所在的两个 level2 List 是同一个 level1 List里的。
那么:repetition level 标示着新 List 出现的层级:
下图能够看出,换句话说就是 repetition level 告诉咱们,在从列式表达,还原嵌套结构的时候,是在哪一级插入新值的.
repetiton = 0,标志着一整条新 record 的开始. 在扁平化结构里,没有 repetition 因此 repetition level 老是 0. Only levels that are repeated need a Repetition level: optional 和 required 永远也不会重复,在计算 repetition level 的时候,可将其跳过.
message AddressBook {
required string owner;
repeated string ownerPhoneNumbers;
repeated group contacts {
required string name;
optional string phoneNumber;
}
}
如今咱们同时用这两种标识(definition level, repetition level),从新考虑 Address book 的例子. 下表显示了每一列 两种标识可能出现的最大值,并解释了为何要比列所在深度小.
单说 contacts.phoneNumber 这一列,若是 手机号有定义,则 definition level 达到最大即2,若是有一个联系人是没有手机号的,则 definition level是 1. 若是联系人是空的,则 definition level 是0.
AddressBook {
owner: "Julien Le Dem",
ownerPhoneNumbers: "555 123 4567",
ownerPhoneNumbers: "555 666 1337",
contacts: {
name: "Dmitriy Ryaboy",
phoneNumber: "555 987 6543",
},
contacts: {
name: "Chris Aniszczyk"
}
}
AddressBook {
owner: "A. Nonymous"
}
如今咱们拿 contacts.phoneNumber 这一列来作说明.
若一条记录是以下这样的:
AddressBook {
contacts: {
phoneNumber: "555 987 6543"
}
contacts: {
}
}
AddressBook {
}
转成列式以后,列中存储的东西应该是这样的(R = Repetiton Level, D = Definition Level):
为了将这条嵌套结构的 record 转换成列式,咱们把这个 record 整个遍历一次,
最后列中存储的东西是:
注意,NULL 值在这里列出来,是为了表述清晰,可是其实是不会存储的. 列中小于最大 definition 值的(这个例子里最大值是2),都应该是 NULL.
为了经过列是存储,还原重建这条嵌套结构的记录,写一个循环读列中的值,
高效存储 Definition Levels 和 Repetiton Levels.
在存储方面,问题很容易归结为:每个基本类型的列,都要建立三个子列(R, D, Value). 然而,得益于咱们所采用的这种列式的格式,三个子列的总开销其实并不大. 由于两种 Levels的最大值,是由 schema 的深度决定的,而且一般只用几个 bit 就够用了(1个bit 就可表达1层嵌套,2个bit就能够表达3层嵌套了,3个bit就可以表达7层嵌套了, [ 译注:四层嵌套编程的时候就已经很恶心了,从编程和可维护角度,也不该该搞的嵌套层次太深(我的观点) ]),对于上面的 AddressBook 实例,owner这一列,深度为1,contacts.name 深度为2,而这个表达能力已经很强了. R level 和 D level 的下限 老是0,上限老是列的深度. 若是一个 field 不是 repeated 的,就更好了,能够不须要 repetition level,而 required field 则不须要 definition level,这下降了两种 level 的上限.
考虑特殊状况,全部 field 全是 required(至关于SQL 中的NOT NULL),repetition level 和 definition level 就彻底不须要了(老是0,因此不须要存储),直接存值就ok了. 若是咱们要同时支持存储扁平结构,那么两种 level也是同样不须要存储空间的.
因为以上这些特性,咱们能够找到一种结合 Run Length Encoding 和 bit packing(https://github.com/Parquet/parquet-mr/tree/master/parquet-column/src/main/java/parquet/column/values/rle) 的高效的编码方式. 一个不少值为 NULL 的稀疏的列,压缩后几乎不怎么占空间,与此类似,一个几乎老是有值的 optional 列,will cost very little overhead to store millions of 1s(在这个也没想好怎么翻译,总之是开销很小的意思了). 现实情况是,用于存储 levels 的空间,能够忽略不计. 以存储一个扁平结构为例(没有嵌套),直接顺序地把一列的值写入,若是某个field是 optional 的,那就取一位用来标识是否为 null.
完.
对于Parquet 里面的具体实现,实在不想读Java,有时间再看好了,或许也会补上 RLE + bit packing 的相关说明,以及示例代码.