一、Parquet文件的格式拆解
概念
Row Group
A logical horizontal partitioning of the data into rows. There is no physical structure that is guaranteed for a row group. A row group consists of a column chunk for each column in the dataset.
简单来说就是将一块数据横着切分为几块,每一块叫做一个Row Group。
Column Chunk
A chunk of the data for a particular column. They live in a particular row group and are guaranteed to be contiguous in the file.
简单来说就是在切分完行组之后,又竖着切分,其中每一列的数据所构成的分片就被称为Column Chunk,最后再把这些Column Chunk顺序地保存。
Page
Column chunks are divided up into pages. A page is conceptually an indivisible unit (in terms of compression and encoding). There can be multiple page types which are interleaved in a column chunk.
把Column Chunk再横着切分,每一块成为一个Page,每个Page的默认大小为1M。
💡 前两步切分还能理解,这次切分是为了什么?
猜想:可能是为了让数据读取的粒度足够小,便于单条数据或小批量数据的查询。因为Page是Parquet文件最小的读取单位,同时也是压缩的单位,如果没有Page这一级别,压缩就只能对整个Column Chunk进行压缩,而Column Chunk如果整个被压缩,就无法从中间读取数据,只能把Column Chunk整个读出来之后解压,才能读到其中的数据。
文件格式
Header
Header的内容很少,只有4个字节,本质是一个magic number,用来指示文件类型。这个magic number目前有两种变体,分别是“PAR1”和“PARE”。其中“PAR1”代表的是普通的Parquet文件,“PARE”代表的是加密过的Parquet文件。
Data Block
Index
https://parquet.apache.org/docs/file-format/pageindex/#technical-approach
参考官方文档说明,后续迭代已经把这一部分索引独立出来和Row Group分开存储,靠近Footer。
目前Parquet的索引有两种,一种是Max-Min统计信息,一种是BloomFilter。其中Max-Min索引是对每个Page都记录它所含数据的最大值和最小值,这样某个Page是否不满足查询条件就可以通过这个Page的max和min值来判断。BloomFilter索引则是对Max-Min索引的补充,针对value比较稀疏,max-min范围比较大的列,用Max-Min索引的效果就不太好,BloomFilter可以克服这一点,同时也可以用于单条数据的查询。
Footer
Footer是Parquet元数据的大本营,包含了诸如schema,Block的offset和size,Column Chunk的offset和size等所有重要的元数据。另外Footer还承担了整个文件入口的职责,读取Parquet文件的第一步就是读取Footer信息,转换成元数据之后,再根据这些元数据跳转到对应的block和column,读取真正所要的数据。
关于Footer还有一个问题,就是为什么Parquet要把元数据放在文件的末尾而不是开头?这主要是为了让文件写入的操作可以在一趟(one pass)内完成。因为很多元数据的信息需要把文件基本写完以后才知道(例如总行数,各个Block的offset等),如果要写在文件开头,就必须seek回文件的初始位置,大部分文件系统并不支持这种写入操作(例如HDFS)。而如果写在文件末尾,那么整个写入过程就不需要任何回退。
二、Parquet是如何“知道”要跳过/扫描哪个行组的
很显然,在Footer中维护了Data部分的元数据,引擎可以根据每个Page的max和min值,选择是否要跳过这个Page,不用读取这部分数据,也就减少了IO的开销。
三、Parquet高效压缩的秘密
字典编码
假设有个字段name,在10条数据中的值分别为:
name:
0, 1, 0, 2, 0, 2, 1, 3, 1, 0
dictionary:
0 -> bruce, 1 -> cake, 2 -> kevin, 3 -> leo
这种方式在很多开源软件中都有使用,比如Elasticsearch,有两个优点:
- 可以节省存储空间
- 可以根据dictionary中的内容,过滤掉不符合条件的数据。在Parquet中,我们可以根据字符编码的特性来做相应的过滤。通过Column Metada中的信息,读取相应的Dictionary Page进行对比,从而过滤掉不符合条件的数据。
查询语句: SELECT name, school FROM student WHERE name = "leo"
File 0
Row Group 0, Column 0 -> 0: bruce, 1:cake
Row Group 1, Column 0 -> 0: bruce, 2:kevin
File 1
Row Group 0, Column 0 -> 0: bruce, 1:cake, 2: kevin
Row Group 1, Column 0 -> 0: bruce, 1:cake, 3: leo
通过对比过滤条件name = "leo",只需要加载File 1的Row Group 1中的数据。
基于位压缩的运行长度编码
场景
| 压缩算法 | 使用场景 |
|---|---|
| Dictionary Encoding | 小规模的数据集合,例如 IP 地址 |
| Run Length Encoding | 重复数据 |
| Delta Encoding | 有序数据集,例如 timestamp,自动生成的 ID,以及监控的各种 metrics |
| Prefix Encoding | Delta Encoding for strings |