最终,我静下心来学习Parquet。
引言
想象如果我有一个待办事项列表,里面包含了我想写的话题,Apache Parquet已经在列表里待了一段时间了。
本周,我从待办事项列表中拿出Parquet,掸去了厚厚的灰尘,并承诺开始深入研究这种文件格式。
你正在阅读的文章是我在了解这种文件格式结构及其读写协议后提炼出的内容。
概述
在处理大型数据集时,数据结构可以决定其存储和访问的效率。
传统的行式格式将数据存储为记录,一条接一条,和数据库表格类似。
行式格式,由作者创建本图片
这种格式直观,并且在需要频繁访问整个记录时效果非常好。
然而,在处理分析时,它效率不高,因为通常只需要从大型数据集中提取特定列。
例如,想象一个有50列和数百万行的表。如果只对其中的3列感兴趣,行式格式需要读取每一行的全部50列。
列式格式将数据存储为列而非行来解决这一问题。
这意味着当需要特定列时,只需要读取需要的数据,从而显著减少了扫描的数据量。
列式格式,由作者创建本图片
然而,简单地以列式格式存储数据也有缺点。记录的写入或更新操作需要涉及多个列段,导致大量的I/O操作。在处理大型数据集时,这会显著降低写入性能。
此外,当查询多列时,数据库系统必须从单独的列中重建记录,这种重建的成本随着查询中的列数增加而增加。
混合格式结合了二者的优点。
混合格式,由作者创建本图片
混合格式将数据分组到“行组”中,每个行组包含数据的一个子集(水平分区)。在每个行组内,将每列的数据称为一个“列块”(垂直分区)。
在行组内,确保这些块在磁盘上连续存储。
过去,我以为Parquet纯粹是一种列式格式,相信许多人可能也这么认为。更确切地说,Parquet以混合格式组织数据。
将在下一节深入探讨Parquet文件结构。
由作者创建本图片
Parquet文件由以下组成:
Parquet是一种自描述文件格式,包含应用程序所需的所有信息,它使得软件能高效地理解和处理文件,而不需要外部信息。因此,元数据是Parquet的关键部分:
Parquet元数据模型。来源
魔数:魔数是位于文件开头和结尾的特定字节序列(PAR1),用于验证它是否是一个有效的Parquet文件。
文件元数据:Parquet在文件的页脚中存储文件元数据。这些元数据提供诸如行数、数据模式和行组元数据等信息。本质上,每个行组元数据包含其列块(列元数据)的信息,如编码和压缩方案,未压缩/压缩大小,页面偏移量,值的数量,以及列块的最小/最大值。当导航Parquet文件时,应用程序可以使用元数据中的信息来限制数据扫描;它可以根据过滤器修剪不必要的行组,或选择只读取所需的列。
页面头:页面头元数据与页面数据一起存储,包括值编码、定义编码和重复编码等信息。除了数据值,Parquet还存储数据的重复级别,以处理嵌套数据。应用程序使用页面头来读取和解码数据。
Google Dremel(BigQuery背后的查询引擎)启发了Parquet实现嵌套和重复字段存储的方法。在2010年介绍Dremel的论文中,Google详细说明了使用定义级别(用于嵌套字段)和重复级别(用于类似数组的字段)在分析工作负载中高效处理嵌套和重复字段的方法。我在七个月前写了一篇关于这种方法的文章,可以在这里读到它:
为了更好地理解Parquet在幕后如何存储数据,我写了一个Python程序,将一个Pandas数据框写入Parquet文件,并使用fastparquet读取这个文件。我尽量保持过程简单以快速理解这一想法。因此,文件写入过程中设有并行配置,它将一个10行的数据框写入一个单一的Parquet文件。读取过程也很简单,读取这个文件时,除了文件路径外没有任何参数。
在下一节中,我将描述对Parquet写入和读取数据过程的理解。
Parquet格式中的数据是如何写入的?
将使用“Parquet写入器”这个术语指代以Parquet格式写入数据的过程。
以下是将数据集写入parquet文件的过程:
Parquet写入过程,由作者创建本图片
应用程序发出写入请求,参数数据包括:每列的压缩模式(可选)、每列的编码模式(可选)、文件模式(写入一个文件或多个文件)、自定义元数据等。
Parquet写入器首先收集诸多信息,如数据模式、空值出现、编码模式和所有列类型,这些信息均记录在文件元数据中。
接下来,写入器在文件开头写入魔数。
然后,它根据行组的最大大小(可配置)和数据大小计算行组的数量,这一步还决定了数据的哪个子集属于哪个行组。之后,开启每个行组的物理写入过程。
对于每个行组,它遍历列列表以写入行组的每个列块,这一步将使用用户指定的压缩方案(默认为无)在写入块时压缩数据。
块写入过程首先计算每页的行数,使用最大页面大小和块大小。接下来,将计算列的最小值/最大值统计数据。(计算只适用于具有可测量类型的列,如整数或浮点数。)
然后,逐页顺序写入列表。每个页面都有一个头,包括页面的行数、页面的数据编码、重复和定义。如果该列使用了字典编码,字典页面会存储在数据页面之前。字典页面也有相关的页面头。
写入所有页面后,Parquet写入器构建该块的列块元数据,包括列的最小值/最大值(如果有)、total_uncompressed_size、total_compressed_size、第一个数据页面偏移量、第一个字典页面偏移量等信息。
继续列块写入过程,直到将行组中的所有列都写入磁盘,确保列块连续存储。每个列块的元数据记录在行组元数据中。
写入所有行组后,所有行组的元数据均已记录在文件元数据中。
文件元数据已写入页脚。
在文件末尾写入魔数,结束整个写入过程。
Parquet格式中的数据的读取过程又是如何的呢?
将使用“Parquet读取器”这个术语来指代负责读取Parquet数据文件的过程。
以下是读取parquet文件的过程:
应用程序发出读取请求,读取参数包括输入文件、用于限制读取行组数量的过滤器、所需的列集等。
如果应用程序需要验证它正在读取parquet文件的有效性,读取器将寻找文件的第一和最后四个字节,检查文件开头和结尾是否有魔数。
然后,它尝试从页脚读取文件元数据,提取出稍后需要使用的信息,如文件模式和行组元数据。
如果指定了过滤器,过滤器将限制扫描的行为。因为行组中包含所有列块的元数据,包括每个可测量列块的最小值/最大值统计数据;读取器只需要遍历每个行组并检查过滤器与每个块的统计数据。如果满足过滤器的限定要求,这个行组被添加到行组列表中,稍后用于读取。如果没有过滤器,列表中将包含全部行组。
接下来,读取器定义列列表。如果应用程序指定了它想要读取的列的子集,列表只包含这些列。否则,列表包含所有列。
下一步是读取行组。读取器将遍历行组列表并读取每个行组。
读取器将根据列表读取每个行组的列块,它使用列元数据读取块。
当第一次读取列块时,读取器使用列元数据中的第一个页面偏移量定位第一个数据页面(或如果使用字典编码则为字典页面)的位置。从这个位置开始,读取器顺序读取页面,直到没有页面为止。为了知道是否有剩余数据,读取器跟踪当前读取的行数,并将其与块的总行数进行比较。如果两个数字相等,读取器已经读取了全部块数据。
为了读取和解码每个数据页面,读取器访问页面头以收集编码信息,如值编码、定义和重复级别编码。
读取完所有行组的列块后,读取器将读取下一个行组。
该过程继续,直到读取完列表中的所有行组为止。
观察
我的观察
多文件
应用程序可以指定写入过程模式,从而将数据集输出到多个文件,甚至可以指定分区标准,以便该过程可以将parquet输出文件组织到Hive分区文件夹中。例如,将2024-08-01的所有数据都存储在文件夹date=2024-08-01中,将2024-08-02的所有数据都存储在文件夹date=2024-08-02中。
并行性
由于Parquet文件可以存储在多个文件中,应用程序可以同时使用多线程读取它们。
此外,单个Parquet文件在水平(行组)和垂直(列块)上进行分区,应用程序可使用多线程在行组或列级别并行读取数据。
编码
Parquet中的列块的数据在行组中紧密存储在一起。这有助于Parquet更有效地编码数据,因为同一列中的数据往往更加同质和重复。
Parquet利用诸如字典编码和运行长度编码(RLE)等技术显著减少存储空间。在字典编码之后,数据在Parquet中进一步运行长度编码。
字典编码用较短的唯一键替换重复值,减少冗余并改善压缩。据我所知,在Parquet中默认实现字典编码。如果数据满足预定义条件(如不同值的数量),应用程序将可以直接应用它。
另一方面,RLE通过存储值及其重复计数来压缩连续相同的值,这些方法最小化了存储的数据量,并通过减少需要扫描的数据量来优化读取性能。
OLAP工作负载
使用统计数据过滤行组并选择仅读取所需列可以显著化工作负载。给出以下查询:
图片由carbon.now.sh创建
有了以下Parquet布局,只需要读取行组1和2,专注于每个行组中的列A和B,无需读取所有列。
由作者创建本图片
尾声
以上就是我了解到的关于Parquet的数据结构。我计划将来写更多深入探讨这种文件格式的文章,请继续关注我的未来作品。
顺便说一下,我对Parquet的经验有限,所以我对这种格式的视角可能不够宽泛。如果你发现我遗漏了什么或想要进一步讨论,请通过LinkedIn、电子邮件或Twitter直接与我联系。
[1] Anastassia Ailamaki, David J. DeWitt, Mark D. Hill, Marios Skounakis, Weaving Relations for Cache Performance[3] Wes McKinney, Extreme IO performance with parallel Apache Parquet in Python (2017)[4] Michael Berk, Demystifying the Parquet File Format (2022)[5] fastparquet源代码GitHub仓库I spent 8 hours learning Parquet. Here’s whatI discoveredhttps://medium.com/data-engineer-things/i-spent-8-hours-learning-parquet-heres-what-i-discovered-97add13fb28f
本文由Vu Trinh撰写, Data Engineer Things的作者关注我的时事通讯 vutr.substack.com ,订阅每周写作,关于OLAP数据库和其他数据工程主题。