Parquet文件格式,已成为大数据存储的默认选择
有天晚上加完班,我在公司楼下等外卖,旁边测试妹子问我一句:你们数仓那边为什么老让我把文件转成 parquet?csv 不香吗?
我当时愣了一下,发现平时写代码全靠“肌肉记忆”:df.to_parquet() 一敲完事,反而没跟大家好好讲过,为什么现在大数据这摊东西里,Parquet 会变成默认选项。
索性那天回去我就整理了下,把这些年踩坑的感受用大白话说一说,顺手用 Python 写点例子,你以后再遇到 parquet 这个格式,脑子里就不会只剩下四个字:能跑就行。
行存还是列存,先搞清楚这事
你可以先脑补一张很长很长的 Excel 表。
行存(CSV、JSON):一行是一个完整“对象”,一整行挨在一起往磁盘上写。 列存(Parquet):是把“每一列”的值凑一起存。
举个特别直白的例子,有个订单表:
id, user_id, amount, city
1, 1001, 88.5, "上海"
2, 1002, 12.0, "北京"
3, 1003, 199.9, "上海"
...
如果是行存,磁盘上大概是这样:
1,1001,88.5,"上海"
2,1002,12.0,"北京"
3,1003,199.9,"上海"
...
如果是列存(简化理解一下),磁盘上更像:
id: [1, 2, 3, ...]
user_id:[1001, 1002, 1003, ...]
amount: [88.5, 12.0, 199.9, ...]
city: ["上海", "北京", "上海", ...]
为啥要这么折腾?
因为大数据场景下,最常见的查询其实是“只看几列 + 扫很多行”,比如:
看近 30 天订单金额的分布,只要 amount做城市维度的统计,只用到 city+amount
如果是行存,每读一行都会把不需要的列也从磁盘拉出来;列存的话,只扫你要的那几列,IO 直接省一大截,这就是 Parquet 能吃香的第一原因。
Parquet 在磁盘上,大概长什么样
真正的 Parquet 文件结构其实挺复杂的,但你可以用一个比较接地气的版本去记:
整个文件被切成一个个 Row Group,有点像“分批打包”,每包几万行、几十万行。 每个 Row Group 里面,又按列切成 Column Chunk。 Column Chunk 再往里,是一页一页的 Page,里面有数据、有字典、有统计信息。
这些设计,主要解决三个现实问题:
压缩率要高比如城市这一列,一堆“上海”“上海”“北京”,直接存太浪费。 Parquet 会优先做字典编码,把重复值变成数字 ID,再压缩,空间能省非常多。
只读需要的列,不读多余的查询只要
amount,那读取引擎看到元数据就知道:这个 Row Group 里amount在文件的哪个位置、多长,直接跳过去读这段就行了。能快速跳过“没用的数据块”每一列的 Page 上会带个统计值,比如 min/max/count。 比如你查“金额 > 100”的订单,某个 Page 的
amount.max = 50,引擎可以一眼判断“这页肯定不符合条件”,就不读了,这就是大家常说的 谓词下推 + 跳过扫描。
所以,Parquet 的本质可以粗暴理解成:带着脑子的压缩列存文件格式——知道自己哪里有什么数据,还会“看一眼条件再决定要不要读”。
用 Python 随手玩一下 Parquet
先来点轻松的,直接用 pandas + pyarrow 写一个数据集到 Parquet,再读回来。
import pandas as pd
import numpy as np# 构造一份“假”订单数据
n = 10_000
df = pd.DataFrame({
"order_id": np.arange(1, n + 1),
"user_id": np.random.randint(1, 2000, size=n),
"amount": np.round(np.random.exponential(scale=80, size=n), 2),
"city": np.random.choice(["上海", "北京", "广州", "深圳", "杭州"], size=n),
})
# 写成 Parquet 文件(默认用 pyarrow engine)
df.to_parquet("orders.parquet", engine="pyarrow")
print("写完啦,大小:")
import os
print(os.path.getsize("orders.parquet") / 1024, "KB")
# 再读回来
df2 = pd.read_parquet("orders.parquet", engine="pyarrow")
print(df2.head())
你可以对比一下,改成 to_csv("orders.csv") 再对比文件大小,一般数据量一上去,Parquet 通常小一大截。
只读几列 + 加条件,Parquet 的优势就出来了
假设有个典型分析需求:只想看上海订单的金额分布,不关心其他列。代码上其实就两行差别:
import pandas as pd# 只读部分列
df_sh_amount = pd.read_parquet(
"orders.parquet",
columns=["amount", "city"]
)
print(df_sh_amount.head())
如果数据在本地磁盘,你可以自己粗暴测试一下时间差,或者:
先读全量所有列,再 df[df["city"] == "上海"]对比“只读几列 + 加条件”的耗时
真正的“大头”开销其实都在 IO 上,Parquet 这种列存格式,配合只读必要列、配合下推过滤条件,性能差异就会很肉眼可见。
Partition(分区目录)这一招,跟 Parquet 是绝配
在数据湖、Hive、Spark 那一套里,你经常会看到这样的路径:
/warehouse/orders/
dt=2024-12-01/city=上海/part-0001.parquet
dt=2024-12-01/city=北京/part-0002.parquet
dt=2024-12-02/city=上海/part-0003.parquet
...
这个目录结构其实就是“分区字段”。 配合 Parquet 的列存,你会得到两个好处:
目录就已经过滤了一遍——你查 dt='2024-12-01' and city='上海',文件系统层面就帮你排除掉一大堆目录。文件内部再过滤一遍——进了文件之后,Parquet 做“按列存 + 统计信息过滤”。
用 Python 写个简单版的分区输出,可以这么搞:
import pandas as pd
import numpy as npn = 20_000
df = pd.DataFrame({
"order_id": np.arange(1, n + 1),
"user_id": np.random.randint(1, 2000, size=n),
"amount": np.round(np.random.exponential(scale=80, size=n), 2),
"dt": np.random.choice(["2024-12-24", "2024-12-25"], size=n),
"city": np.random.choice(["上海", "北京", "广州"], size=n),
})
# 写成按 dt, city 分区的集合
df.to_parquet(
"lake/orders",
engine="pyarrow",
partition_cols=["dt", "city"]
)
写完你去 lake/orders 下面看目录,会发现已经帮你按字段拆好了文件夹,这玩意跟 Spark / Hive / Trino 那一堆统统兼容。
Schema & 类型这件事,Parquet 会比 CSV 老实很多
搞数据久了你会发现,CSV/JSON 最烦的一点就是:类型全靠猜。
"00123"到底是字符串还是数字?null是值,还是字面量?布尔是 true/false还是1/0还是"Y"/"N"?
Parquet 里,Schema 是写死在文件里的,类似:
order_id: INT64amount: DOUBLEcity: BYTE_ARRAY (UTF8)dt: BYTE_ARRAY (UTF8)
引擎在读的时候不需要“猜”,也不会因为某一行写了 "abc" 导致整个列类型变得奇奇怪怪。对下游(比如 Spark、Flink、Pandas)来说:读出来的列是什么类型,基本是确定的。
你甚至可以用 pyarrow 直接定义一个 Schema 再写入:
import pyarrow as pa
import pyarrow.parquet as pqtable = pa.table({
"order_id": pa.array([1, 2, 3], type=pa.int64()),
"amount": pa.array([10.5, 20.0, 30.2], type=pa.float64()),
"city": pa.array(["上海", "北京", "深圳"], type=pa.string())
})
schema = pa.schema([
("order_id", pa.int64()),
("amount", pa.float64()),
("city", pa.string()),
])
pq.write_table(table, "orders_arrow.parquet", schema=schema)
这个在跨语言、跨引擎的时候,体验会明显比 CSV 这种“全字符串”的格式靠谱得多。
Schema 变更:加列、删列,Parquet 一般怎么处理
线上的业务字段不可能一成不变,经常会出现:
今天加个 channel渠道字段明天增加一个 is_new_user标记
Parquet 自己是支持 Schema Evolution(模式演进) 的,主要是几个常见情况比较友好:
新增列老文件没有这个列,新文件有。 读取的时候,老文件里的这个字段会被当成
null,新文件正常有值。删除列对读取方来说,只要你不去要那一列,老文件还在也无所谓。
简单用 pandas 模拟一个“新增列”的情况:
import pandas as pd# 老 schema
df_old = pd.DataFrame({
"order_id": [1, 2, 3],
"amount": [10.5, 20.0, 30.2],
})
df_old.to_parquet("orders_v1.parquet")
# 新 schema,多了一个 channel
df_new = pd.DataFrame({
"order_id": [4, 5],
"amount": [40.0, 50.0],
"channel": ["app", "h5"],
})
df_new.to_parquet("orders_v2.parquet")
# 读多个文件,pandas 会自动 schema 对齐
df_all = pd.read_parquet(["orders_v1.parquet", "orders_v2.parquet"])
print(df_all)
你会看到前几行 channel 是 NaN,后面的有值,这就是最常见的“加列演进”场景。
真正复杂的是改类型(int 改成 string 之类),不同引擎的处理方式不太一样,一般都会建议你:慎改类型,宁愿加一个新列。
为啥说 Parquet 已经是“大数据存储的默认选择”
站在一个写业务代码的角度,你可以这么理解这句话:
几乎所有主流大数据计算引擎,都把 Parquet 当一等公民Spark、Flink、Hive、Trino、Presto、Impala、ClickHouse(外表)、各种云数仓…… 你用这些工具的时候,配置里不写格式,默认就是 parquet 的情况越来越多。
跟对象存储搭起来刚好HDFS 现在很多公司就不再大规模部署了,直接上对象存储(S3 / OSS / COS)。 对象存储 + Parquet + 目录分区,基本就能凑出一整套“数据湖”的底座。
工程团队之间,约定 Parquet 当“交换格式”,会省掉很多扯皮比如:
中间少掉了大量“CSV 字段类型不对”“编码不一致”的扯皮时间。
实时计算团队把统计好的指标写到 /warehouse/xxx/下的 Parquet数仓团队拿来直接跑离线 算法团队直接用 Python 读 Parquet 做特征工程
压缩 + 列存带来的成本优势是非常实打实的同样一份数据,假设:
硬成本是:存储钱 + 读写 IO 的钱,云环境里都是直接体现在账单上的。
CSV 100 GB 压缩 CSV 30 GB Parquet 10 GB(甚至更小,看字段特点)
Python 项目里,什么场景适合用 Parquet
你平时写 Python 业务,也不一定都是大数据平台,但只要碰到下面几种情况,我一般都会优先选 Parquet:
中台/服务之间需要落一个“临时数据集”比如某批量任务把结果先写到文件,给另一个任务用,如果字段比较多、数据量偏大,直接
to_parquet()一把梭。做离线报表、分析脚本数据源是数据库,导出来以后反复分析—— 与其每次重新从 DB 拉一遍,不如导到 Parquet,本地脚本多跑几次也快得多。
给算法同学准备训练数据特别是特征维度大的那种表,用 CSV 往往会变成“加载很慢 + 占空间”,不如 Parquet 一步到位。
说它好用归好用,但真落地也有一些现实中的坑,简单提几条你有个概念就行:
文件太碎一堆特别小的 parquet 文件(比如几十 KB),会让计算引擎很难受。 尽量控制每个文件几十 MB 上百 MB 这个量级比较舒服。
分区字段乱选如果你按高基数字段(比如 user_id)分区,会得到海量小目录+小文件,效果适得其反。 一般选日期、地区、业务线之类相对稳定的维度做分区。
Schema 随便改特别是线上不停加字段、改类型,久而久之同一路径下文件的 schema 非常混乱,下游读取就会很痛苦。 最好把“schema”当做一种契约去管理。
差不多就先聊到这。你下次在项目里看到“统一用 Parquet 吧”这种决策,大概就知道背后的考量:不是“时髦”,而是它在性能、压缩、类型、安全感这些方面,刚好满足了大数据绝大多数的需求。你要是有具体场景想用 Parquet,不确定该怎么落,可以把场景丢给我,我们可以一起把代码捋一捋。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB