作者创建的图片。
这篇文章最初发布于 https://vutr.substack.com。
我有一个虚拟的任务列表,里面包含了我想写的话题,而Apache Parquet已经在那里放了很久了。
这周,我把 Parquet 从待办事项中拿出来,拂去厚厚的灰尘,并承诺深入研究这种文件格式。
你正在阅读的文章是我学习了这种文件格式结构及其读写协议后的全部总结。
当处理大型数据集时,数据的结构会决定它能够被存储和访问的效率。
传统的行存储格式将数据以记录的形式依次存储,很像一个数据库表。
行式格式。由作者创建的图像。
这种格式直观且在频繁访问整个记录时表现良好。然而,在进行数据分析时,通常只需要从大型数据集中获取特定列,这时它可能会变得效率低下。
例如,想象一个有50列和数百万行的表。如果你只对分析其中的3列感兴趣,采用行级格式仍然需要为每一行读取所有的50列。
列式格式通过将数据按列而不是按行存储来解决此问题。这意味着当你需要特定的列时,你只需读取所需的列数据,从而显著减少了扫描的数据量。
列式格式。由作者创建的图像。
然而,仅仅以列式格式存储数据有一些缺点。记录的写入或更新操作需要触碰多个列段,导致大量的I/O操作。这会显著降低写入性能,特别是在处理大规模数据集时。
此外,当查询涉及多个列时,数据库系统必须从单独的列中重新构建记录。这种重建的成本随着查询涉及的列数的增加而增加。
这种混合格式结合了两种格式的优点。
混合格式。作者创建的图片。
该格式将数据分组成“行组”,每个行组包含一部分行。(水平分区。)在每个行组内,每一列的数据称为“列块。”(垂直分区)
在行组中,这些数据块保证在磁盘上连续存储。
在过去,我认为Parquet纯粹是一种列式格式,我相信你们中的许多人可能也有同样的想法。更准确地说,Parquet在幕后以一种混合格式组织数据。
我们将在这部分内容中深入探讨Parquet文件结构。
术语和元数据
作者创建的图像。
一个 Parquet 文件由以下部分组成:
Parquet 是一种自描述文件格式,包含了消费该文件的应用程序所需的所有信息。这使得软件能够高效地理解和处理文件,而无需额外的外部信息。因此,元数据是 Parquet 的关键部分:
Parquet 元数据模型。来源
PAR1
),位于文件的开头和结尾。该数字用于验证文件是否为有效的Parquet文件。谷歌的 Dremel(BigQuery 背后的查询引擎)启发了 Parquet 实现嵌套和重复字段存储的方法。在一篇于 2010 年发表的文章 中,谷歌详细介绍了其在分析工作负载中高效处理嵌套和重复字段的方法,使用定义级别(用于嵌套字段)和重复级别(用于类似数组的字段)。我七个月前写了一篇文章介绍这种方法,你可以在这里阅读:
为了更好地理解Parquet文件背后的数据存储方式,我编写了一个Python程序,将一个Pandas数据框写入Parquet文件,并使用fastparquet读取该文件。我尽量保持过程简单,以便快速理解这个概念。因此,在文件写入过程中没有使用任何配置,如多线程,只是将一个包含10行的数据框写入了一个单独的Parquet文件。读取过程也很简单,除了文件路径外,没有使用任何参数。
在接下来的部分,我将阐述我对 Parquet 写入和读取数据过程的理解。
我将使用“Parquet Writer”这一术语来指代负责以Parquet格式写入数据的过程。
以下是将数据集写入 Parquet 文件的概述过程:
Parquet 写入过程。作者创建的图片。
FileMetadata
中。FileMetadata
中。FileMetadata
写入文件尾部。我将使用“Parquet Reader”这一术语来指代负责读取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(运行长度编码)通过只存储一个值及其重复次数来压缩连续相同的值。这些方法通过减少需要扫描的数据量来最小化存储的数据量并优化读取性能。
使用统计信息过滤行组并仅选择所需读取的列,可以显著提升分析工作负载的性能。给定以下查询:
创建于 carbon.now.sh
使用以下 Parquet 布局,我们只需要读取行组 1 和 2,关注每个行组中的列 A 和 B,而不是读取所有列。
作者创建的图像。
以上是我对 Parquet 的所有了解。我计划在未来撰写更多与此文件格式相关的深入文章。所以敬请期待我的后续作品 ;)
顺便说一下,我对 Parquet 的了解有限,可能无法从更广泛的视角来看待这种格式。如果您觉得我遗漏了某些内容或想要进一步讨论,请在评论区留言或直接通过 LinkedIn、电子邮件 或 Twitter 联系我。
[1] Anastassia Ailamaki, David J. DeWitt, Mark D. Hill, Marios Skounakis,为缓存性能编织关系
[2]Parquet 官方文档
[3] Wes McKinney,使用多线程的Python中的并行Apache Parquet实现极致IO性能 (2017)
[4] Michael Berk,揭秘Parquet文件格式 (2022)
[5]fastparquet 源代码 GitHub 仓库