Parquet仅仅是一种存储格式,它是语言、平台无关的,而且不须要和任何一种数据处理框架绑定,目前可以和Parquet适配的组件包括下面这些,能够看出基本上一般使用的查询引擎和计算框架都已适配,而且能够很方便的将其它序列化工具生成的数据转换成Parquet格式。java
Parquet项目由如下几个子项目组成:git
下图展现了Parquet各个组件的层次以及从上到下交互的方式。github
Parquet支持嵌套的数据模型,相似于Protocol Buffers,每个数据模型的schema包含多个字段,每个字段又能够包含多个字段,每个字段有三个属性:重复数、数据类型和字段名,重复数能够是如下三种:required(出现1次),repeated(出现0次或屡次),optional(出现0次或1次)。每个字段的数据类型能够分红两种:group(复杂类型)和primitive(基本类型)。例如Dremel中提供的Document的schema示例,它的定义以下:算法
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转换成树状结构,根节点能够理解为repeated类型,以下图: 数据库
能够看出在Schema中全部的基本类型字段都是叶子节点,在这个Schema中一共存在6个叶子节点,若是把这样的Schema转换成扁平式的关系模型,就能够理解为该表包含六个列。Parquet中没有Map、Array这样的复杂数据结构,可是能够经过repeated和group组合来实现这样的需求。在这个包含6个字段的表中有如下几个字段和每一条记录中它们可能出现的次数:apache
DocId int64 只能出现一次 Links.Backward int64 可能出现任意屡次,可是若是出现0次则须要使用NULL标识 Links.Forward int64 同上 Name.Language.Code string 同上 Name.Language.Country string 同上 Name.Url string 同上
因为在一个表中可能存在出现任意屡次的列,对于这些列须要标示出现屡次或者等于NULL的状况,它是由Striping/Assembly算法实现的。编程
上文介绍了Parquet的数据模型,在Document中存在多个非required列,因为Parquet一条记录的数据分散的存储在不一样的列中,如何组合不一样的列值组成一条记录是由Striping/Assembly算法决定的,在该算法中列的每个值都包含三部分:value、repetition level和definition level。缓存
为了支持repeated类型的节点,在写入的时候该值等于它和前面的值在哪一层节点是不共享的。在读取的时候根据该值能够推导出哪一层上须要建立一个新的节点,例如对于这样的一个schema和两条记录。数据结构
message nested { repeated group leve1 { repeated string leve2; } } r1:[[a,b,c,] , [d,e,f,g]] r2:[[h] , [i,j]]
计算repetition level值的过程以下:app
根据以上的分析每个value须要记录的repeated level值以下:
在读取的时候,顺序的读取每个值,而后根据它的repeated level建立对象,当读取value=a时repeated level=0,表示须要建立一个新的根节点(新记录),value=b时repeated level=2,表示须要建立一个新的level2节点,value=d时repeated level=1,表示须要建立一个新的level1节点,当全部列读取完成以后能够建立一条新的记录。本例中当读取文件构建每条记录的结果以下:
能够看出repeated level=0表示一条记录的开始,而且repeated level的值只是针对路径上的repeated类型的节点,所以在计算该值的时候能够忽略非repeated类型的节点,在写入的时候将其理解为该节点和路径上的哪个repeated节点是不共享的,读取的时候将其理解为须要在哪一层建立一个新的repeated节点,这样的话每一列最大的repeated level值就等于路径上的repeated节点的个数(不包括根节点)。减少repeated level的好处可以使得在存储使用更加紧凑的编码方式,节省存储空间。
有了repeated level咱们就能够构造出一个记录了,为何还须要definition levels呢?因为repeated和optional类型的存在,可能一条记录中某一列是没有值的,假设咱们不记录这样的值就会致使本该属于下一条记录的值被当作当前记录的一部分,从而形成数据的错误,所以对于这种状况须要一个占位符标示这种状况。
definition level的值仅仅对于空值是有效的,表示在该值的路径上第几层开始是未定义的,对于非空的值它是没有意义的,由于非空值在叶子节点是定义的,全部的父节点也确定是定义的,所以它老是等于该列最大的definition levels。例以下面的schema。
message ExampleDefinitionLevel { optional group a { optional group b { optional string c; } } }
它包含一个列a.b.c,这个列的的每个节点都是optional类型的,当c被定义时a和b确定都是已定义的,当c未定义时咱们就须要标示出在从哪一层开始时未定义的,以下面的值:
因为definition level只须要考虑未定义的值,而对于repeated类型的节点,只要父节点是已定义的,该节点就必须定义(例如Document中的DocId,每一条记录都该列都必须有值,一样对于Language节点,只要它定义了Code必须有值),因此计算definition level的值时能够忽略路径上的required节点,这样能够减少definition level的最大值,优化存储。
本节咱们使用Dremel论文中给的Document示例和给定的两个值r1和r2展现计算repeated level和definition level的过程,这里把未定义的值记录为NULL,使用R表示repeated level,D表示definition level。
首先看DocuId这一列,对于r1,DocId=10,因为它是记录的开始而且是已定义的,因此R=0,D=0,一样r2中的DocId=20,R=0,D=0。
对于Links.Forward这一列,在r1中,它是未定义的可是Links是已定义的,而且是该记录中的第一个值,因此R=0,D=1,在r1中该列有两个值,value1=10,R=0(记录中该列的第一个值),D=2(该列的最大definition level)。
对于Name.Url这一列,r1中它有三个值,分别为url1=’http://A‘,它是r1中该列的第一个值而且是定义的,因此R=0,D=2;value2=’http://B‘,和上一个值value1在Name这一层是不相同的,因此R=1,D=2;value3=NULL,和上一个值value2在Name这一层是不相同的,因此R=1,但它是未定义的,而Name这一层是定义的,因此D=1。r2中该列只有一个值value3=’http://C‘,R=0,D=2.
最后看一下Name.Language.Code这一列,r1中有4个值,value1=’en-us’,它是r1中的第一个值而且是已定义的,因此R=0,D=2(因为Code是required类型,这一列repeated level的最大值等于2);value2=’en’,它和value1在Language这个节点是不共享的,因此R=2,D=2;value3=NULL,它是未定义的,可是它和前一个值在Name这个节点是不共享的,在Name这个节点是已定义的,因此R=1,D=1;value4=’en-gb’,它和前一个值在Name这一层不共享,因此R=1,D=2。在r2中该列有一个值,它是未定义的,可是Name这一层是已定义的,因此R=0,D=1.
Parquet文件是以二进制方式存储的,因此是不能够直接读取的,文件中包括该文件的数据和元数据,所以Parquet格式文件是自解析的。在HDFS文件系统和Parquet文件中存在以下几个概念。
一般状况下,在存储Parquet数据的时候会按照Block大小设置行组的大小,因为通常状况下每个Mapper任务处理数据的最小单位是一个Block,这样能够把每个行组由一个Mapper任务处理,增大任务执行并行度。Parquet文件的格式以下图所示
上图展现了一个Parquet文件的内容,一个文件中能够存储多个行组,文件的首位都是该文件的Magic Code,用于校验它是否为一个Parquet文件,Footer length了文件元数据的大小,经过该值和文件长度能够计算出元数据的偏移量,文件的元数据中包括每个行组的元数据信息和该文件存储数据的Schema信息。除了文件中每个行组的元数据,每一页的开始都会存储该页的元数据,在Parquet中,有三种类型的页:数据页、字典页和索引页。数据页用于存储当前行组中该列的值,字典页存储该列值的编码字典,每个列块中最多包含一个字典页,索引页用来存储当前行组下该列的索引,目前Parquet中还不支持索引页,可是在后面的版本中增长。
在执行MR任务的时候可能存在多个Mapper任务的输入是同一个Parquet文件的状况,每个Mapper经过InputSplit标示处理的文件范围,若是多个InputSplit跨越了一个Row Group,Parquet可以保证一个Row Group只会被一个Mapper任务处理。
说到列式存储的优点,映射下推是最突出的,它意味着在获取表中原始数据时只须要扫描查询中须要的列,因为每一列的全部值都是连续存储的,因此分区取出每一列的全部值就能够实现TableScan算子,而避免扫描整个表文件内容。
在Parquet中原生就支持映射下推,执行查询的时候能够经过Configuration传递须要读取的列的信息,这些列必须是Schema的子集,映射每次会扫描一个Row Group的数据,而后一次性得将该Row Group里全部须要的列的Cloumn Chunk都读取到内存中,每次读取一个Row Group的数据可以大大下降随机读的次数,除此以外,Parquet在读取的时候会考虑列是否连续,若是某些须要的列是存储位置是连续的,那么一次读操做就能够把多个列的数据读取到内存。
在数据库之类的查询系统中最经常使用的优化手段就是谓词下推了,经过将一些过滤条件尽量的在最底层执行能够减小每一层交互的数据量,从而提高性能,例如”select count(1) from A Join B on A.id = B.id where A.a > 10 and B.b < 100”SQL查询中,在处理Join操做以前须要首先对A和B执行TableScan操做,而后再进行Join,再执行过滤,最后计算聚合函数返回,可是若是把过滤条件A.a > 10和B.b < 100分别移到A表的TableScan和B表的TableScan的时候执行,能够大大下降Join操做的输入数据。
不管是行式存储仍是列式存储,均可以在将过滤条件在读取一条记录以后执行以判断该记录是否须要返回给调用者,在Parquet作了更进一步的优化,优化的方法时对每个Row Group的每个Column Chunk在存储的时候都计算对应的统计信息,包括该Column Chunk的最大值、最小值和空值个数。经过这些统计值和该列的过滤条件能够判断该Row Group是否须要扫描。另外Parquet将来还会增长诸如Bloom Filter和Index等优化数据,更加有效的完成谓词下推。
在使用Parquet的时候能够经过以下两种策略提高查询性能:一、相似于关系数据库的主键,对须要频繁过滤的列设置为有序的,这样在导入数据的时候会根据该列的顺序存储数据,这样能够最大化的利用最大值、最小值实现谓词下推。二、减少行组大小和页大小,这样增长跳过整个行组的可能性,可是此时须要权衡因为压缩和编码效率降低带来的I/O负载。
相比传统的行式存储,Hadoop生态圈近年来也涌现出诸如RC、ORC、Parquet的列式存储格式,它们的性能优点主要体如今两个方面:一、更高的压缩比,因为相同类型的数据更容易针对不一样类型的列使用高效的编码和压缩方式。二、更小的I/O操做,因为映射下推和谓词下推的使用,能够减小一大部分没必要要的数据扫描,尤为是表结构比较庞大的时候更加明显,由此也可以带来更好的查询性能
上图是展现了使用不一样格式存储TPC-H和TPC-DS数据集中两个表数据的文件大小对比,能够看出Parquet较之于其余的二进制文件存储格式可以更有效的利用存储空间,而新版本的Parquet(2.0版本)使用了更加高效的页存储方式,进一步的提高存储空间
上图展现了Twitter在Impala中使用不一样格式文件执行TPC-DS基准测试的结果,测试结果能够看出Parquet较之于其余的行式存储格式有较明显的性能提高。
上图展现了criteo公司在Hive中使用ORC和Parquet两种列式存储格式执行TPC-DS基准测试的结果,测试结果能够看出在数据存储方面,两种存储格式在都是用snappy压缩的状况下量中存储格式占用的空间相差并不大,查询的结果显示Parquet格式稍好于ORC格式,二者在功能上也都有优缺点,Parquet原生支持嵌套式数据结构,而ORC对此支持的较差,这种复杂的Schema查询也相对较差;而Parquet不支持数据的修改和ACID,可是ORC对此提供支持,可是在OLAP环境下不多会对单条数据修改,更多的则是批量导入。
自从2012年由Twitter和Cloudera共同研发Parquet开始,该项目一直处于高速发展之中,而且在项目之初就将其贡献给开源社区,2013年,Criteo公司加入开发而且向Hive社区提交了向hive集成Parquet的patch(HIVE-5783),在Hive 0.13版本以后正式加入了Parquet的支持;以后愈来愈多的查询引擎对此进行支持,也进一步带动了Parquet的发展。
目前Parquet正处于向2.0版本迈进的阶段,在新的版本中实现了新的Page存储格式,针对不一样的类型优化编码算法,另外丰富了支持的原始类型,增长了Decimal、Timestamp等类型的支持,增长更加丰富的统计信息,例如Bloon Filter,可以尽量得将谓词下推在元数据层完成。
本文介绍了一种支持嵌套数据模型对的列式存储系统Parquet,做为大数据系统中OLAP查询的优化方案,它已经被多种查询引擎原生支持,而且部分高性能引擎将其做为默认的文件存储格式。经过数据编码和压缩,以及映射下推和谓词下推功能,Parquet的性能也较之其它文件格式有所提高,能够预见,随着数据模型的丰富和Ad hoc查询的需求,Parquet将会被更普遍的使用。