原力注入

大数据基础之 Parquet 文件格式解析

Parquet 文件格式深入解析

随着大数据和人工智能技术的迅猛发展,数据存储和处理的需求呈指数级增长。传统的存储方式,尤其是关系型数据库,已经无法满足现代应用的需求。特别是在处理非结构化数据(如视频、图像、日志等)时,传统方法显得力不从心。在这种背景下,Parquet 文件格式应运而生,并迅速成为大规模数据存储的事实标准。

什么是 Parquet?

Parquet 是一种开源的列式存储文件格式,专为高效存储和处理大规模数据而设计。它最初由 Apache 软件基金会开发,现已成为大数据生态系统中的重要组成部分。Parquet 的设计目标是优化数据读取性能,减少存储空间占用,并支持复杂的数据类型。

参考文章:

Parquet 的核心优势

  1. 1. 列式存储
    与传统的行式存储不同,Parquet 采用列式存储方式。这意味着数据按列而不是按行存储。对于分析型查询(OLAP),列式存储可以显著提高性能,因为查询通常只需要访问部分列,而不是整行数据。通过跳过不必要的列,Parquet 能够大幅减少 I/O 操作,从而加速查询。

  2. 2. 高效的数据压缩
    Parquet 支持多种压缩算法(如 Snappy、GZIP 等),并通过字典编码和运行长度编码(RLE)等技术进一步减少数据体积。例如,字典编码将重复的字符串替换为整数索引,从而节省存储空间。这种压缩机制不仅降低了存储成本,还减少了数据传输的开销。

  3. 3. 与语言无关
    Parquet 文件格式与编程语言无关,开发人员可以使用 Java、Python、C++ 等多种语言来读写 Parquet 文件。这种灵活性使得 Parquet 能够无缝集成到各种大数据处理框架中,如 Apache Spark、Apache Hive 和 Apache Arrow。

  4. 4. 支持复杂数据类型
    Parquet 不仅支持基本的数据类型(如整数、字符串等),还支持嵌套数据结构(如数组、映射等)。这使得 Parquet 非常适合存储半结构化数据(如 JSON、XML)。

  5. 5. 开源与跨平台
    作为开源格式,Parquet 不受任何特定供应商的锁定,用户可以在不同的平台和工具之间自由迁移数据。

Parquet 文件结构详解

Parquet 文件由以下几个部分组成:

  1. 1. 文件头(Magic Number)
    Parquet 文件的开头是一个 4 字节的 Magic Number,值为 PAR1,用于标识文件格式。

  2. 2. 行组(Row Group)
    行组是 Parquet 文件的基本存储单元,包含多个列块(Column Chunk)。每个列块存储一列的数据。

  3. 3. 页脚(Footer)
    页脚包含文件的元数据,如文件架构、压缩方式、行组偏移量等。页脚的长度存储在文件的最后 4 个字节中。

Parquet 文件结构图示

1. Parquet 文件整体结构

+---------------------+
|      Magic Number   |  // 文件头,标识 Parquet 文件格式(4 字节,"PAR1")
+---------------------+
|      Row Group 0    |  // 行组 0,包含多个列块
| +-----------------+ |
| |  Column Chunk 1 | |  // 列块 1,存储一列的数据
| +-----------------+ |
| |  Column Chunk 2 | |  // 列块 2,存储一列的数据
| +-----------------+ |
| |       ...       | |
+---------------------+
|      Row Group 1    |  // 行组 1,包含多个列块
| +-----------------+ |
| |  Column Chunk 1 | |
| +-----------------+ |
| |  Column Chunk 2 | |
| +-----------------+ |
| |       ...       | |
+---------------------+
|        ...          |  // 更多行组
+---------------------+
|        Footer       |  // 页脚,包含元数据和行组偏移量
+---------------------+
|   Footer Length     |  // 页脚长度(4 字节)
+---------------------+

2. 行组(Row Group)结构

+---------------------+
|      Row Group      |
| +-----------------+ |
| |  Column Chunk 1 | |  // 列块 1,存储一列的数据
| | +-------------+ | |
| | |   Page 1    | | |  // 数据页 1,存储实际数据
| | +-------------+ | |
| | |   Page 2    | | |  // 数据页 2,存储实际数据
| | +-------------+ | |
| |       ...       | |
| +-----------------+ |
| |  Column Chunk 2 | |  // 列块 2,存储一列的数据
| | +-------------+ | |
| | |   Page 1    | | |
| | +-------------+ | |
| | |   Page 2    | | |
| | +-------------+ | |
| |       ...       | |
| +-----------------+ |
|        ...          |  // 更多列块
+---------------------+

3. 列块(Column Chunk)结构

+---------------------+
|     Column Chunk    |
| +-----------------+ |
| |      Page 1     | |  // 数据页 1,存储实际数据
| | +-------------+ | |
| | |   Header    | | |  // 页头,包含页的元数据
| | +-------------+ | |
| | |    Data     | | |  // 数据部分,存储压缩后的数据
| | +-------------+ | |
| +-----------------+ |
| |      Page 2     | |  // 数据页 2,存储实际数据
| | +-------------+ | |
| | |   Header    | | |
| | +-------------+ | |
| | |    Data     | | |
| | +-------------+ | |
| +-----------------+ |
|        ...          |  // 更多数据页
+---------------------+

4. 页(Page)结构

+---------------------+
|        Page         |
| +-----------------+ |
| |      Header     | |  // 页头,包含页的元数据
| | +-------------+ | |
| | |  Page Type  | | |  // 页类型(数据页或字典页)
| | +-------------+ | |
| | |  Encoding   | | |  // 编码方式(如 PLAIN、RLE 等)
| | +-------------+ | |
| | |  Data Size  | | |  // 数据部分的大小
| | +-------------+ | |
| +-----------------+ |
| |      Data      | |  // 数据部分,存储压缩后的数据
| +-----------------+ |
+---------------------+

5. 页脚(Footer)结构

+---------------------+
|        Footer       |
| +-----------------+ |
| |  File Metadata  | |  // 文件元数据,包括文件架构、压缩方式等
| +-----------------+ |
| | Row Group Offset| |  // 行组偏移量,记录每个行组的起始位置
| +-----------------+ |
| |  Column Metadata| |  // 列元数据,记录每列的统计信息
| +-----------------+ |
| |       ...       | |  // 其他元数据
+---------------------+

6. Parquet 文件示例图示

以下是一个具体的 Parquet 文件示例,包含两行数据:

+---------------------+
|      Magic Number   |  // "PAR1"
+---------------------+
|      Row Group 0    |
| +-----------------+ |
| |  Column Chunk 1 | |  // 用户ID [1, 2]
| +-----------------+ |
| |  Column Chunk 2 | |  // 用户名 ["Alice", "Bob"]
| +-----------------+ |
| |  Column Chunk 3 | |  // 年龄 [25, 30]
| +-----------------+ |
| |  Column Chunk 4 | |  // 城市 ["北京", "上海"]
| +-----------------+ |
+---------------------+
|        Footer       |  // 文件架构、压缩方式、行组偏移量
+---------------------+
|   Footer Length     |  // 页脚长度
+---------------------+

7. Parquet 文件的 16 进制表示

以下是一个简化的 Parquet 文件的 16 进制表示:

50 41 52 31  // Magic Number: "PAR1"
15 00 15 80  // Row Group 0: Column Chunk 用户ID
15 80 15 80  // Row Group 0: Column Chunk 用户名
15 80 15 80  // Row Group 0: Column Chunk 年龄
15 80 15 80  // Row Group 0: Column Chunk 城市
15 80 15 80  // Footer: 文件架构
15 80 15 80  // Footer: 压缩方式
15 80 15 80  // Footer: 行组偏移量
15 80 15 80  // Footer: 其他元数据
15 80 15 80  // Footer: 页脚长度

Image

Parquet 文件的读写操作

以下通过 Python 的 pyarrow 库演示如何读写 Parquet 文件。

1. 写入 Parquet 文件

import pyarrow as pa
import pyarrow.parquet as pq

# 定义数据
data = {
    "用户ID": [1, 2, 3],
    "用户名": ["Alice", "Bob", "Charlie"],
    "年龄": [25, 30, 35],
    "城市": ["北京", "上海", "广州"]
}

# 创建 PyArrow 表
table = pa.Table.from_pydict(data)  # 将 Python 字典转换为 PyArrow 表

# 写入 Parquet 文件
pq.write_table(table, 'example.parquet')  # 将表写入 Parquet 文件

2. 读取 Parquet 文件

# 读取 Parquet 文件
table = pq.read_table('example.parquet')

# 转换为 Pandas DataFrame
df = table.to_pandas()
print(df)

输出结果:

用户ID     用户名  年龄  城市
0      1    Alice  25  北京
1      2      Bob  30  上海
2      3  Charlie  35  广州

3. 查询优化:投影和谓词下推

Parquet 支持投影(只读取需要的列)和谓词下推(过滤不需要的行),从而优化查询性能。

# 只读取“用户名”和“城市”列
table = pq.read_table('example.parquet', columns=["用户名", "城市"])

# 过滤年龄大于 30 的行
table = pq.read_table('example.parquet', filters=[("年龄", ">", 30)])

# 转换为 Pandas DataFrame
df = table.to_pandas()
print(df)

输出结果:

      用户名  城市
0  Charlie  广州

Parquet 文件的高级特性

  1. 1. 字典编码
    对于重复值较多的列(如“城市”),Parquet 会使用字典编码来压缩数据。例如,将“北京”、“上海”、“广州”映射为整数索引,从而减少存储空间。

  2. 2. 运行长度编码(RLE)
    对于连续重复的值(如“年龄”),Parquet 会使用 RLE 来进一步压缩数据。例如,如果某列的值为 [25, 25, 25, 30, 30],RLE 会将其编码为 (25, 3), (30, 2)。

  3. 3. 数据跳过
    Parquet 文件的元数据记录了每个行组的最小值和最大值,查询引擎可以根据这些信息跳过不相关的行组。例如,如果查询条件为 年龄 > 30,而某个行组的年龄范围为 [20, 25],查询引擎可以跳过该行组,从而减少 I/O 操作。

  4. 4. 分区存储
    在大数据场景中,Parquet 文件通常按分区存储。例如,按日期分区存储日志数据,查询时只需读取相关分区的文件。

Parquet 的应用场景

  1. 1. 大数据分析
    Parquet 是 Apache Spark、Apache Hive 等大数据工具的默认存储格式,适合用于 OLAP 场景。

  2. 2. 数据湖
    Parquet 是 Delta Lake、Apache Iceberg 等数据湖格式的基础,支持 ACID 事务和版本控制。

  3. 3. 机器学习
    Parquet 支持复杂数据类型(如数组、映射),适合存储特征工程和模型训练数据。

总结

Parquet 文件格式凭借其列式存储、高效压缩和查询优化等特性,已经成为现代数据存储的核心技术。通过本文的示例和操作细节,您可以更好地理解 Parquet 的工作原理,并将其应用到实际的数据处理任务中。无论是大数据分析、数据湖构建,还是机器学习,Parquet 都能提供卓越的性能和灵活性。未来,随着云原生技术的普及,Parquet 有望在更多场景中发挥其优势,成为数据存储和处理的首选格式。