page contents

Parquet文件格式,已成为大数据存储的默认选择

我把文件转成 parquet?csv 不香吗?我当时愣了一下,发现平时写代码全靠“肌肉记忆”:df.to_parquet() 一敲完事,反而没跟大家好好讲过,为什么现在大数据这摊东西里,Parquet 会变成默认选项。索性那天回去我就整理了下,把这些年踩坑的感受用大白话说一说,顺手用 Python 写点例子,你以后再遇到 parquet 这个格式,脑子里就不会只剩下四个字:能跑就行。

attachments-2026-01-pQg1dcUr69659df7202fb.png我把文件转成 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,里面有数据、有字典、有统计信息。

这些设计,主要解决三个现实问题:

  1. 压缩率要高比如城市这一列,一堆“上海”“上海”“北京”,直接存太浪费。 Parquet 会优先做字典编码,把重复值变成数字 ID,再压缩,空间能省非常多。

  2. 只读需要的列,不读多余的查询只要 amount,那读取引擎看到元数据就知道:这个 Row Group 里 amount 在文件的哪个位置、多长,直接跳过去读这段就行了。

  3. 能快速跳过“没用的数据块”每一列的 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 的列存,你会得到两个好处:

  1. 目录就已经过滤了一遍——你查 dt='2024-12-01' and city='上海',文件系统层面就帮你排除掉一大堆目录。
  2. 文件内部再过滤一遍——进了文件之后,Parquet 做“按列存 + 统计信息过滤”。

用 Python 写个简单版的分区输出,可以这么搞:

import pandas as pd import numpy as np n = 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: INT64
  • amount: DOUBLE
  • city: BYTE_ARRAY (UTF8)
  • dt: BYTE_ARRAY (UTF8)

引擎在读的时候不需要“猜”,也不会因为某一行写了 "abc" 导致整个列类型变得奇奇怪怪。对下游(比如 Spark、Flink、Pandas)来说:读出来的列是什么类型,基本是确定的。

你甚至可以用 pyarrow 直接定义一个 Schema 再写入:

import pyarrow as pa import pyarrow.parquet as pq table = 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(模式演进) 的,主要是几个常见情况比较友好:

  1. 新增列老文件没有这个列,新文件有。 读取的时候,老文件里的这个字段会被当成 null,新文件正常有值。

  2. 删除列对读取方来说,只要你不去要那一列,老文件还在也无所谓。

简单用 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 已经是“大数据存储的默认选择”

站在一个写业务代码的角度,你可以这么理解这句话:

  1. 几乎所有主流大数据计算引擎,都把 Parquet 当一等公民Spark、Flink、Hive、Trino、Presto、Impala、ClickHouse(外表)、各种云数仓…… 你用这些工具的时候,配置里不写格式,默认就是 parquet 的情况越来越多。

  2. 跟对象存储搭起来刚好HDFS 现在很多公司就不再大规模部署了,直接上对象存储(S3 / OSS / COS)。 对象存储 + Parquet + 目录分区,基本就能凑出一整套“数据湖”的底座。

  3. 工程团队之间,约定 Parquet 当“交换格式”,会省掉很多扯皮比如:

    中间少掉了大量“CSV 字段类型不对”“编码不一致”的扯皮时间。

  • 实时计算团队把统计好的指标写到 /warehouse/xxx/ 下的 Parquet
  • 数仓团队拿来直接跑离线
  • 算法团队直接用 Python 读 Parquet 做特征工程

压缩 + 列存带来的成本优势是非常实打实的同样一份数据,假设:

硬成本是:存储钱 + 读写 IO 的钱,云环境里都是直接体现在账单上的。

  • CSV 100 GB
  • 压缩 CSV 30 GB
  • Parquet 10 GB(甚至更小,看字段特点)

Python 项目里,什么场景适合用 Parquet

你平时写 Python 业务,也不一定都是大数据平台,但只要碰到下面几种情况,我一般都会优先选 Parquet:

  1. 中台/服务之间需要落一个“临时数据集”比如某批量任务把结果先写到文件,给另一个任务用,如果字段比较多、数据量偏大,直接 to_parquet() 一把梭。

  2. 做离线报表、分析脚本数据源是数据库,导出来以后反复分析—— 与其每次重新从 DB 拉一遍,不如导到 Parquet,本地脚本多跑几次也快得多。

  3. 给算法同学准备训练数据特别是特征维度大的那种表,用 CSV 往往会变成“加载很慢 + 占空间”,不如 Parquet 一步到位。

说它好用归好用,但真落地也有一些现实中的坑,简单提几条你有个概念就行:

  • 文件太碎一堆特别小的 parquet 文件(比如几十 KB),会让计算引擎很难受。 尽量控制每个文件几十 MB 上百 MB 这个量级比较舒服。

  • 分区字段乱选如果你按高基数字段(比如 user_id)分区,会得到海量小目录+小文件,效果适得其反。 一般选日期、地区、业务线之类相对稳定的维度做分区。

  • Schema 随便改特别是线上不停加字段、改类型,久而久之同一路径下文件的 schema 非常混乱,下游读取就会很痛苦。 最好把“schema”当做一种契约去管理。

差不多就先聊到这。你下次在项目里看到“统一用 Parquet 吧”这种决策,大概就知道背后的考量:不是“时髦”,而是它在性能、压缩、类型、安全感这些方面,刚好满足了大数据绝大多数的需求。你要是有具体场景想用 Parquet,不确定该怎么落,可以把场景丢给我,我们可以一起把代码捋一捋。

更多相关技术内容咨询欢迎前往并持续关注好学星城论坛了解详情。

想高效系统的学习Python编程语言,推荐大家关注一个微信公众号:Python编程学习圈。每天分享行业资讯、技术干货供大家阅读,关注即可免费领取整套Python入门到进阶的学习资料以及教程,感兴趣的小伙伴赶紧行动起来吧。

attachments-2022-05-rLS4AIF8628ee5f3b7e12.jpg

  • 发表于 2026-01-13 09:21
  • 阅读 ( 39 )
  • 分类:Python开发

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1783 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 2228 文章
  3. Pack 1783 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章