parquet存储格式

parquet是一种列存储格式, 是 Apache 开源的一种列式存储文件格式,主要用在大数据和机器学习的数据处理里。Spark、Pandas、PyArrow、DuckDB、Hugging Face Datasets 都能直接读写它。

1. Intro

**核心特点:按列存储。**CSV、JSON 这类格式是一行一行存的,Parquet 则把同一列的数据放在一起存。比如一张表有 id、text、score 三列:

行式存储的顺序是

复制代码
(1, "hello", 0.9), (2, "world", 0.8), ...

列式存储的顺序是

复制代码
[1, 2, ...]
["hello", "world", ...]
[0.9, 0.8, ...]

列式存储的好处:

  1. 只读需要的列:如果只要 score,就不用把整列 text 从磁盘读出来,I/O 大幅减少。
  2. 压缩率高:同一列的类型相同、值也相似,配合字典编码、游程编码(RLE),再加上 Snappy、Zstd 等压缩算法,文件通常比 CSV 小好几倍。
  3. 自带 schema:文件里记录了每列的类型,比如 int64、string 或嵌套结构,读的时候不用猜类型。
  4. 可以跳过无关数据:文件分成多个行组(row group),每个行组都存有统计信息,比如每列的最小值和最大值。按条件过滤时,可以直接跳过不可能有匹配行的行组。
  5. 适合分布式处理:一个大数据集通常切成很多个 Parquet 分片,多个 worker 可以并行读取。

缺点:

  1. 不适合频繁地单行更新或追加写入,因为文件写完基本就不再改动。
  2. 二进制格式,不能像 CSV 那样直接用文本编辑器查看。

2. row groups, column chunk和page

Parquet 文件的结构分四层,从外到内是:

文件(File)→ 行组(Row Group)→ 列块(Column Chunk)→ 页(Page)

Parquet 文件

├── 魔数 "PAR1"

├── Row Group 0

│ ├── Column Chunk: id → Page, Page, ...

│ ├── Column Chunk: text → Page, Page, ...

│ └── Column Chunk: score → Page, Page, ...

├── Row Group 1

│ └── ...

├── Footer(元数据:schema、各行组和列块的位置与统计信息)

└── 魔数 "PAR1"

Row Group(行组)

行组先把整张表按行横向切开。比如 1000 万行的数据可以分成 10 个行组,每个行组 100 万行。行组内部再按列存储,每一列的数据叫一个列块(Column Chunk)。

所以 Parquet 实际上是"先按行分组,组内再按列存"。这样既有列式存储的好处,又不需要一次把整列都读进内存。

行组的作用:

最小的并行和读取单位:Spark、PyArrow 这类引擎通常按行组分配任务,一个 worker 读一个或几个行组。

用统计信息跳过数据:footer 里记录了每个行组中每个列块的最小值、最大值和 null 数量。比如查询 score > 0.95 时,如果某个行组里 score 的最大值只有 0.8,整个行组都可以直接跳过。

决定内存占用:写文件时,一个行组的数据要先在内存里攒满才能写出去;读的时候,通常也是一次处理一个行组。

行组大小怎么选:

太大(比如几个 GB)会导致读写内存占用高,并行度也低。

太小会让元数据变多、压缩率下降,读取时随机 I/O 也更多。

常见做法是让每个行组在 128MB 到 1GB 之间,或者在几十万到一百万行左右。PyArrow 用 row_group_size 参数按行数控制。

Page(页)

每个列块又被切成若干页。页是编码和压缩的最小单位,默认大小约 1MB。

常见的页类型:

Data Page(数据页):存实际的列值,以及处理 null 和嵌套结构用的 repetition/definition levels。

Dictionary Page(字典页):如果某列用了字典编码,列块开头会有一个字典页,记录"编号 → 真实值"的映射。后面的数据页只存编号,对重复值多的列(比如 language、source)压缩效果特别好。

页的作用:

编码和压缩以页为单位:读取时可以逐页解压,不用一次解压整个列块。

更细粒度的跳读:较新版本的 Parquet 支持 Page Index,记录了每个页的最小值和最大值。这样过滤时不仅能跳过行组,还能在行组内部跳过页。

各层职责

层级 怎么切分 主要作用

Row Group

按行横切

并行读取、按统计信息跳过、控制内存

Column Chunk

行组内按列

只读需要的列

Page

列块内再切

编码和压缩、更细粒度的跳读

Footer

文件末尾

存 schema 和所有位置、统计信息,读文件时最先读它

元数据放在文件末尾,是因为写文件时要等所有数据写完,才知道每一块的位置和统计信息。所以读 Parquet 时,会先读取文件结尾的 footer,再按需定位到具体的行组和列块。

python 复制代码
"""Write a few rows to Parquet, then read them back in several ways."""
import pyarrow as pa
import pyarrow.compute as pc
import pyarrow.parquet as pq
PATH = "demo.parquet"
NUM_ROWS = 10
ROW_GROUP_SIZE = 4
# 1. 构造数据:一列整数、一列文本、一列浮点、一列重复值多的类别
rows = [
    {
        "id": i,
        "text": f"document number {i} " + "lorem ipsum " * (i + 1),
        "score": round(i / NUM_ROWS, 2),
        "source": ["web", "books", "code"][i % 3],
    }
    for i in range(NUM_ROWS)
]
table = pa.Table.from_pylist(rows)
print("=== 原始 schema ===")
print(table.schema, "\n")
# 2. 写文件:每 4 行一个行组,数据页上限设得很小,让列块被切成多个页
pq.write_table(
    table,
    PATH,
    row_group_size=ROW_GROUP_SIZE,
    data_page_size=64,  # 字节;默认约 1MB
    compression="zstd",
    write_statistics=True,
    write_page_index=True,
)
# 3. 读元数据:先读 footer
pf = pq.ParquetFile(PATH)
meta = pf.metadata
print("=== 文件元数据 ===")
print(f"总行数={meta.num_rows}  行组数={meta.num_row_groups}  列数={meta.num_columns}\n")
print("=== 每个行组 / 列块 ===")
for rg_idx in range(meta.num_row_groups):
    rg = meta.row_group(rg_idx)
    print(f"Row Group {rg_idx}: {rg.num_rows} 行, {rg.total_byte_size} 字节(未压缩)")
    for col_idx in range(rg.num_columns):
        col = rg.column(col_idx)
        stats = col.statistics
        print(
            f"  {col.path_in_schema:<7} "
            f"编码={col.encodings}  压缩后={col.total_compressed_size}B  "
            f"min={stats.min!r}  max={stats.max!r}  "
            f"有字典页={col.has_dictionary_page}"
        )
print()
# 4. 全量读取
print("=== 全量读取 ===")
print(pq.read_table(PATH).to_pandas(), "\n")
# 5. 只读一个行组的部分列
print("=== 只读 Row Group 1 的 id 和 source ===")
print(pf.read_row_group(1, columns=["id", "source"]).to_pandas(), "\n")
# 6. 流式读取:每批 3 行
print("=== iter_batches(batch_size=3) ===")
for batch in pf.iter_batches(batch_size=3, columns=["id", "score"]):
    print(f"  batch: id={batch.column('id').to_pylist()}  score={batch.column('score').to_pylist()}")
print()
# 7. 带过滤条件读取:score >= 0.7 时,Row Group 0 (max=0.3) 可以直接跳过
print("=== 过滤读取 score >= 0.7 ===")
filtered = pq.read_table(PATH, columns=["id", "score"], filters=[("score", ">=", 0.7)])
print(filtered.to_pandas(), "\n")
# 8. 校验读回的数据与原始数据一致
roundtrip = pq.read_table(PATH)
assert roundtrip.equals(table), "读回的数据和原始数据不一致"
assert pc.sum(roundtrip["id"]).as_py() == sum(range(NUM_ROWS))
print("往返校验通过:读回的数据与写入的完全一致")
相关推荐
humbinal3 个月前
同时支持 gui & cli 的 parquet 文件查看工具,高性能小清新!
hive·python·rust·spark·开源·github·parquet
l1t4 个月前
DeepSeek总结的parquet Variant “碎形化“技术
数据库·parquet
七夜zippoe6 个月前
DolphinDB数据导入导出:CSV、JSON、Parquet
物联网·json·csv·parquet·dolphindb
achi0106 个月前
GCP BigQuery 导出:CSV 与二进制格式(Parquet/Avro)全维度深度对比
csv·gcp·parquet·bigquery·avro·google cloud·bigquery 导出格式
喜欢吃豆1 年前
Parquet 范式:大语言模型训练数据格式优化的基础解析
人工智能·语言模型·自然语言处理·大模型·parquet
亚林瓜子1 年前
在mac上面使用parquet-cli查看parquet文件
hadoop·macos·cli·parquet
柯大侠爱喝水2 年前
python pandas ,处理csv文件、hdf5文件、parquet文件效率详细对比
python·pandas·csv·hdf5·parquet
archimekai3 年前
Google Dremel和parquet的复杂嵌套数据结构表征方法解析
数据结构·数据库·google·parquet