找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2328

积分

1

好友

321

主题
发表于 4 天前 | 查看: 14| 回复: 0

你有没有发现,现在但凡与大数据分析相关的场景,最终存储格式十有八九都会落到 .parquet 文件上。无论是使用 Hive 做数仓查询,还是用 Spark 处理数据,甚至只是本地进行一些小型分析,常常会听到这样的建议:“别再用 CSV 了,直接用 Parquet 吧。”

为什么它几乎成了大数据存储的“默认选项”?这背后其实有非常清晰的技术逻辑,一点也不玄学。

我先分享一个真实的场景。某天深夜,业务方临时提出需要查询一批历史订单数据,总量高达几十亿条。当时数据是以 text 格式配合 gzip 压缩存储的,在 Hive 中执行全表扫描查询,等待时间长得让人绝望。后来,我们将这批历史表迁移到了 Parquet 格式。当业务方再次执行相同条件的查询时,体验发生了天壤之别——几乎是命令刚提交,结果就出来了,快到让人来不及刷手机。

那一刻,我彻底被 Parquet 的效率折服了。

那么,Parquet 究竟是什么?

简单来说,它是一个 列式存储 的文件格式,并附带一套严谨的 元数据规范。它本身并非数据库,更像是一个“为各类大数据计算引擎设计的高性能文件格式标准”。

用大白话理解“行存”与“列存”

假设我们有一张订单表,包含以下字段:

  • id
  • user_id
  • amount
  • status
  • created_at

传统的行式存储(如 CSV、JSON 或多数关系型数据库的表)是这样工作的:数据按行写入磁盘。当你想读取 amount 字段时,系统需要把整行数据都加载出来,其他无关字段也被顺带读取了。

列式存储(以 Parquet 为代表)则完全不同:

  • 它将 id 这一列的所有值 连续地存储在一起
  • user_id 列也单独存储在一块。
  • amount 列同样如此。
  • ……

因此,当你只需要查询其中两三个字段时,磁盘只需扫描对应的那几列数据,其他列完全不用触碰。这种“只读取所需数据”的特性,正是 Parquet 在大数据场景下性能卓越的首要原因。

大数据场景为何偏爱 Parquet?

我们不照本宣科,就从实际应用的角度聊聊。

1. 仅扫描所需列,大幅减少磁盘 I/O
想象一个最常见的分析需求:运营想看“每日用户下单金额 Top N”。
你真正关心的字段只有:

  • user_id
  • amount
  • created_at

如果底层数据是 CSV:

  • 必须读取表中每一行的完整数据。
  • 然后在内存中丢弃不需要的字段。

如果底层数据是 Parquet:

  • 引擎可以直接定位并只读取这三列对应的数据块。
  • 其他列的数据完全不会被访问。

在大数据处理中,磁盘 I/O 往往是最大的性能瓶颈和成本之一。少读一半的列,就意味着直接减少一半的 I/O,性能提升立竿见影,无需复杂的调优技巧。

2. 压缩效率极高,既省空间又提速
列式存储带来一个附带优势:同一列的数据值通常非常相似
例如 status 字段,无非是 “PAID”、“CREATED”、“CANCELLED” 等值的重复,非常适合字典编码压缩。created_at 这类时间戳具有明显的递增趋势,也极易压缩。

Parquet 按列进行压缩,远比压缩整行数据能获得更高的压缩比。而且,压缩不仅是为了节省硬盘空间,还能间接提升读取速度——因为从磁盘读出的数据量本身就变小了。解压工作由 CPU 完成,而大多数大数据作业是 I/O 受限而非 CPU 受限,因此整体耗时反而更短。

在写入 Parquet 时,可以指定压缩算法:

import pandas as pd

df = pd.DataFrame({
    "user_id": [1, 2, 3, 4],
    "amount": [10.5, 20.3, 7.8, 100.0],
    "status": ["PAID", "PAID", "CREATED", "CANCELLED"],
})

# 使用 pyarrow 引擎写入 parquet,并指定压缩算法
df.to_parquet(
    "orders.parquet",
    engine="pyarrow",         # 常用引擎
    compression="snappy"      # 常用选项:snappy / gzip / zstd
)

snappy 是一个均衡的选择:压缩速度极快,压缩率适中;gzip 能获得更高的压缩率,但 CPU 消耗更大;zstd 是近年来流行的新选择,在压缩率和速度之间取得了更好的平衡。

3. 天生为分析型查询而生
行式存储适合“点查”:

  • 按主键查询单条记录。
  • 更新某一行数据。

而列式存储则更擅长“扫描大量数据并进行聚合”:

  • GROUP BY
  • SUM / AVG / COUNT
  • 多维分析、OLAP、指标看板等场景。

因此,像 Hive、Spark、Flink 这类偏向离线批处理或分析的 大数据 计算引擎,都与 Parquet 格式深度集成。你会发现,在线事务处理(OLTP)数据库和离线分析数仓,在存储设计上通常是完全不同的思路。

Parquet 文件内部结构简述

不必被 “RowGroup”、“ColumnChunk” 这些术语吓到,可以将其想象成一个多层的收纳盒:

  • 最外层:一个 Parquet 文件。
  • 文件内被切分为多个 Row Group(行组,可理解为按批次打包的数据块)。
  • 每个 Row Group 内,再按列切成 Column Chunk(列块)。
  • Column Chunk 内部又分为 Page,用于更细粒度的编码和统计。

最关键的部分是 文件尾部的元数据。这里存储了:

  • 每列的模式(Schema):名称、类型、是否可为空等。
  • 每列在每个 Row Group 中的统计信息(最小值、最大值、空值数量等)。
  • 每块数据在文件中的偏移量和长度。

这样做的好处是什么?查询引擎无需读取整个 Parquet 文件,只需先查看元数据。例如,如果查询条件是 amount > 100,引擎会检查每个 Row Group 中 amount 列的 min/max 值。如果某个 Row Group 的 max 值都小于 100,那么整个 Row Group 都可以被跳过,根本不需要读取其中的数据。这就是所谓的 谓词下推(Predicate Pushdown) 优化。

动手实践:用 Python 操作 Parquet

如果你习惯使用 pandas,上手 Parquet 非常容易。

写入示例(含分区):

import pandas as pd

df = pd.DataFrame({
    "user_id": [1001, 1002, 1003, 1004],
    "amount": [9.9, 199.0, 35.5, 800.0],
    "status": ["CREATED", "PAID", "PAID", "REFUND"],
    "dt": ["2025-01-01", "2025-01-01", "2025-01-02", "2025-01-02"],
})

# 按日期分区保存,会生成 dt=2025-01-01/ 这样的目录结构
df.to_parquet(
    "data/orders_parquet",
    engine="pyarrow",
    partition_cols=["dt"],   # 指定分区列
    compression="snappy"
)

读取时,可以利用列裁剪和过滤:

import pandas as pd

# 只读取指定的三列(列裁剪)
df = pd.read_parquet(
    "data/orders_parquet",
    columns=["user_id", "amount", "dt"]
)

# 然后在 pandas 中进行条件过滤
high_value = df[df["amount"] > 100]
print(high_value.head())

如果使用 PySpark,则能更充分地利用底层优化:

from pyspark.sql import SparkSession

spark = SparkSession.builder \
    .appName("parquet-demo") \
    .getOrCreate()

df = spark.read.parquet("hdfs:///warehouse/orders_parquet")

# 选择特定列并附加查询条件
result = (
    df.select("user_id", "amount", "dt")
      .where("dt >= '2025-01-01' and amount > 100")
      .groupBy("dt")
      .sum("amount")
)

result.show()

这里的 where 条件过滤和列选择(select),Spark 会尽可能将其下推到 Parquet 文件的元数据层面进行优化,从而减少实际需要读取的数据量。

不可忽视的 Schema(模式)管理

Parquet 的一个显著优点是 强 Schema
数据并非随意塞入,而是:

  • 每列的数据类型都被明确记录在元数据中。
  • 支持复杂的嵌套结构(如 struct, list, map)。

这对大数据平台至关重要:

  • 跨语言:用 Python 写的 Parquet 文件,Java、Scala、Go 都能正确读取,且类型一致。
  • 跨引擎:Spark 写入的数据,Trino、Flink、Hive、DuckDB、Pandas 等引擎都可以直接读取。

只要约定好统一的 Schema 和分区规则,整个“数据湖”就可以成为多个计算引擎共享的 Parquet 文件目录。

当然,业务 Schema 很少一成不变,常见的变更包括:

  • 新增字段
  • 修改字段类型
  • 废弃旧字段

Parquet 对“新增字段”比较友好:新写入的文件包含新字段,而老文件缺少该字段,读取时自动补为 NULL 即可。但对于“修改字段类型”(如 int 改为 string)则需格外谨慎,部分引擎可能无法自动兼容而直接报错。最佳实践是:尽量通过新增字段来演进 Schema,避免原地修改已有字段的类型。

Parquet 成为“默认选择”的领域

回顾大数据的主流场景,Parquet 几乎无处不在:

  1. 数据仓库 / 数据湖
    • Hive 表:STORED AS PARQUET 已成为常见配置。
    • Spark/Flink 作业:中间结果表、宽表、特征表普遍采用 Parquet。
    • 湖仓一体方案(Iceberg / Delta Lake / Hudi):其底层存储格式大多基于 Parquet,并在此基础上增加了事务、索引、快照等能力。
  2. 日志与埋点数据
    • 数据入口可能是原始的 JSON 或文本。
    • 但经过清洗和转换后,长期存储层通常会转换为 Parquet 格式,以方便后续的分析查询。
  3. 特征存储与机器学习
    • 训练样本往往有数百甚至上千个特征(列)。
    • 单次模型训练通常只用到其中的一个子集。
    • 列式存储 + 列裁剪 + 高效压缩,简直是为此场景量身定做。

如果你查看多数开源大数据项目的文档,在“支持的文件格式”列表中,Parquet 几乎总是位列前茅。

避坑指南:Parquet 的常见陷阱

Parquet 虽好,但也并非银弹,以下几个“坑”需要特别注意:

1. 小文件灾难
最常见的问题是 Spark 等作业写出了 大量体积极小的 Parquet 文件(每个只有几百KB到几MB),导致目录下堆积了数万甚至数十万个文件。
后果是:

  • HDFS 或对象存储的 NameNode 元数据压力剧增。
  • 查询时,打开/关闭无数小文件的开销巨大,严重拖慢速度。
    解决方案:
  • 写入时使用 coalesce()repartition() 控制输出文件数量。
  • 或者,建立定时任务来合并历史小文件。
    例如,在 Spark 写入时进行控制:
    (
    df.repartition(10, “dt”)   # 按日期分区,并控制每个分区的文件数
      .write
      .mode(“overwrite”)
      .partitionBy(“dt”)
      .format(“parquet”)
      .save(“hdfs:///warehouse/orders_parquet”)
    )

2. 过度分区
Parquet 是列存,而分区是在文件/目录级别的“粗粒度”切分。两者结合效果更佳,但分区策略不能过于细致。
例如,按 user_id 或毫秒级时间戳分区,会导致目录数量爆炸,产生海量小文件,反而降低查询性能。
稳妥的分区维度通常是:

  • 日期(年/月/日)或小时。
  • 地域、业务线等较高层级的维度。
    目标是让单个分区内有“足够多”的数据,避免切分过细。

3. Schema 演进管理不当
如前所述,新增字段相对安全,但修改类型风险高。更糟糕的情况是,同一张逻辑表,在不同时间批次的数据中使用了不一致的 Schema。例如,某天的作业额外写入了一个调试字段,但上游的 Hive Metastore 并未同步更新 Schema 定义。
这会导致部分引擎读取出错,部分引擎却能兼容,造成混乱。成熟的团队通常会:

  • 使用集中的 Schema 注册管理(如 AWS Glue、Hive Metastore)。
  • 建立严格的 Schema 变更审批流程。

4. 不适合高频单条更新
这是列式存储的通病,并非 Parquet 独有。它更适用于 追加写入批量重写 的场景,不适合像 OLTP 数据库那样进行高并发的单行随机更新或删除。
因此,如果有实时更新、高并发点查的需求,应该选择专用的数据库,而让 Parquet 专注于离线分析领域。

直观对比:CSV vs Parquet

如果你想在本地有个感性认识,可以运行以下对比代码(请注意,这并非严谨的性能基准测试):

import pandas as pd
import numpy as np
import time

# 模拟一百万行订单数据
n = 1_000_000
df = pd.DataFrame({
    “user_id”: np.random.randint(1, 100_000, size=n),
    “amount”: np.random.rand(n) * 1000,
    “status”: np.random.choice([“CREATED”, “PAID”, “CANCELLED”, “REFUND”], size=n),
    “dt”: np.random.choice([“2025-01-01”, “2025-01-02”, “2025-01-03”], size=n),
})

# 写 CSV
t0 = time.time()
df.to_csv(“orders.csv”, index=False)
print(“csv write:”, time.time() - t0, “s”)

# 写 Parquet
t0 = time.time()
df.to_parquet(“orders.parquet”, engine=“pyarrow”, compression=“snappy”)
print(“parquet write:”, time.time() - t0, “s”)

# 只读三列——对比读取时间
t0 = time.time()
df_csv = pd.read_csv(“orders.csv”, usecols=[“user_id”, “amount”, “dt”])
print(“csv read:”, time.time() - t0, “s”)

t0 = time.time()
df_parquet = pd.read_parquet(“orders.parquet”, columns=[“user_id”, “amount”, “dt”])
print(“parquet read:”, time.time() - t0, “s”)

通常你会观察到:

  • Parquet 文件体积显著小于 CSV 文件(得益于压缩)。
  • 在仅读取部分列时,Parquet 的读取速度明显快于 CSV。

当然,线上生产环境还需考虑网络、分布式执行、数据缓存等诸多因素,但性能优化的基本原理是一致的。希望这篇探讨能帮助你更深入地理解 Parquet,并在实际工作中更好地运用它。更多关于大数据和数据处理技术的深度讨论,欢迎访问云栈社区进行交流。




上一篇:程序员技术创业为何失败?从代码思维到商业思维的5大转变盲区
下一篇:中小团队三台服务器部署:选Kubernetes还是坚持Systemd?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-10 18:18 , Processed in 0.192907 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表