列式存储格式之Parquet
列式存储
列式存储和行式存储相比有哪些优势呢?
- 可以跳过不符合条件的数据,只读取需要的数据,降低 IO 数据量。
- 压缩编码可以降低磁盘存储空间。由于同一列的数据类型是一样的,可以使用更高效的压缩编码(例如 Run Length Encoding 和 Delta Encoding)进一步节约存储空间。
- 只读取需要的列,支持向量运算,能够获取更好的扫描性能。
Parquet是什么
Parquet的灵感来自于2010年Google发表的Dremel论文,文中介绍了一种支持嵌套结构的存储格式,并且使用了列式存储的方式提升查询性能,在Dremel论文中还介绍了Google如何使用这种存储格式实现并行查询的,如果对此感兴趣可以参考论文和开源实现Apache Drill。
Parquet仅仅是一种存储格式,它是语言、平台无关的,并且不需要和任何一种数据处理框架绑定,目前能够和Parquet适配的组件包括下面这些,可以看出基本上通常使用的查询引擎和计算框架都已适配,并且可以很方便的将其它序列化工具生成的数据转换成Parquet格式。
查询引擎: Hive, Impala, Pig, Presto, Drill, Tajo, HAWQ, IBM Big SQL
计算框架: MapReduce, Spark, Cascading, Crunch, Scalding, Kite
数据模型: Avro, Thrift, Protocol Buffers, POJOs
Parquet文件格式
Parquet文件是以二进制方式存储的,所以是不可以直接读取的,文件中包括该文件的数据和元数据,因此Parquet格式文件是自解析的。在HDFS文件系统和Parquet文件中存在如下几个概念。
HDFS块(Block):它是HDFS上的最小的副本单位,HDFS会把一个Block存储在本地的一个文件并且维护分散在不同的机器上的多个副本,通常情况下一个Block的大小为256M、512M等。
HDFS文件(File):一个HDFS的文件,包括数据和元数据,数据分散存储在多个Block中。
行组(Row Group):按照行将数据物理上划分为多个单元,每一个行组包含一定的行数,在一个HDFS文件中至少存储一个行组,Parquet读写的时候会将整个行组缓存在内存中,所以如果每一个行组的大小是由内存大的小决定的,例如记录占用空间比较小的Schema可以在每一个行组中存储更多的行。
列块(Column Chunk):在一个行组中每一列保存在一个列块中,行组中的所有列连续的存储在这个行组文件中。一个列块中的值都是相同类型的,不同的列块可能使用不同的算法进行压缩。
页(Page):每一个列块划分为多个页,一个页是最小的编码的单位,在同一个列块的不同页可能使用不同的编码方式。
数据模型(先理解Parquet文件格式在理解数据模型)
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模式其实是为了实现复杂数据类型如 list array等形式
required只能出现1次
repeated代表可能出现任意多次,但是如果出现0次则需要使用NULL标识
optional代表出现0次或1次
下面代码是从qarquet schema(包含元数据以及数据)获取元数据字段名以及字段类型(相当于sql定义表时表字段名以及字段类型)
package com.didichuxing.fe.offline.util; import com.didichuxing.fe.offline.entity.TableInfo; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.parquet.format.converter.ParquetMetadataConverter; import org.apache.parquet.hadoop.ParquetFileReader; import org.apache.parquet.hadoop.metadata.ParquetMetadata; import org.apache.parquet.schema.MessageType; import org.apache.parquet.schema.Type; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; public class ParquetShema { private static Logger logger = LoggerFactory.getLogger(ParquetShema.class); public static List<TableInfo> getSchema(Configuration conf, Path parquetInputPath){ List<Type> result = new ArrayList<>(); List<TableInfo> tableInfoList = new ArrayList<TableInfo>(); TableInfo tableInfo = new TableInfo(); try { conf.set("fs.hdfs.impl", "org.apache.hadoop.hdfs.DistributedFileSystem"); FileSystem fs = FileSystem.get(conf); FileStatus fileList[] = fs.listStatus(parquetInputPath); FileStatus f = fileList[1]; ParquetMetadata readFooter = null; readFooter = ParquetFileReader.readFooter(conf, f.getPath(), ParquetMetadataConverter.NO_FILTER); MessageType schema =readFooter.getFileMetaData().getSchema(); logger.error(schema.toString()); result = schema.getFields(); logger.error(result.toString()); if (result != null) { for (int i=0; i< result.size(); i++) { // logger.error("column elem: " + result.get(i).getName()+",origin type:" // + (result.get(i).getOriginalType().toString() == null ? "null" : result.get(i).getOriginalType().toString()) // + ", real type:" + (result.get(i).asPrimitiveType().getName() == null ? "null" : result.get(i).asPrimitiveType().getName())); logger.info("column elem: " + result.get(i).getName()); tableInfo.setColumnName(result.get(i).getName()); logger.info("getPrimitiveTypeName: {} getOriginalType: {} ", result.get(i).asPrimitiveType().getPrimitiveTypeName().name(), result.get(i).getOriginalType().toString()); if(result.get(i).getOriginalType().name() == null){ if(result.get(i).asPrimitiveType().getPrimitiveTypeName().name().equals("BINARY")){ tableInfo.setColumnType(ParquetToHiveTypeConst.BINARY); }else if(result.get(i).asPrimitiveType().getPrimitiveTypeName().name().equals("BOOLEAN")){ tableInfo.setColumnType(ParquetToHiveTypeConst.BOOLEAN); }else if(result.get(i).asPrimitiveType().getPrimitiveTypeName().name().equals("DOUBLE")){ tableInfo.setColumnType(ParquetToHiveTypeConst.DOUBLE); }else if(result.get(i).asPrimitiveType().getPrimitiveTypeName().name().equals("FLOAT")){ tableInfo.setColumnType(ParquetToHiveTypeConst.FLOAT); }else if(result.get(i).asPrimitiveType().getPrimitiveTypeName().name().equals("INT32")){//name()方法获取枚举名字 tableInfo.setColumnType(ParquetToHiveTypeConst.INT32); }else if(result.get(i).asPrimitiveType().getPrimitiveTypeName().name().equals("INT64")){ tableInfo.setColumnType(ParquetToHiveTypeConst.INT64); }else if(result.get(i).asPrimitiveType().getPrimitiveTypeName().name().equals("INT96")){ tableInfo.setColumnType(ParquetToHiveTypeConst.INT96); }else{ logger.error("OriginalType转换错误"); } }else if(result.get(i).getOriginalType().name() != null){ if(result.get(i).asPrimitiveType().getPrimitiveTypeName().name().equals("BINARY") && result.get(i).getOriginalType().name().equals("DECIMAL")){ tableInfo.setColumnType(ParquetToHiveTypeConst.DECIMAL); }else if(result.get(i).asPrimitiveType().getPrimitiveTypeName().name().equals("BINARY") && result.get(i).getOriginalType().name().equals("UTF8")){ tableInfo.setColumnType(ParquetToHiveTypeConst.BINARY); }else{ tableInfo.setColumnType(result.get(i).asPrimitiveType().getPrimitiveTypeName().name().toLowerCase()); } }else{ logger.error("PrimitiveType类型转换错误"); } tableInfoList.add(tableInfo); } } } catch (Exception e) { logger.info(e.getMessage()); } return tableInfoList; } }
字段类型里面有PrimitiveType以及OriginalType两种,具体怎么用可以查询这两种用途。
参考:https://blog.csdn.net/yu616568/article/details/50993491 (将parquet的值得好好看看)