大数据文件格式全景:Parquet、ORC、Avro、Protobuf、CSV、JSON 的设计原理与选型指南

发布于 2026-06-11

目录

写在前面:为什么”文件格式”是一个值得专门研究的话题

很多人对数据的认知停留在”数据就是一张表”。但数据要落到磁盘上、要在网络里传输、要被引擎读取计算,它就必须被序列化成某种字节排列——这就是”文件格式”。

同样一张用户表,存成 CSV、JSON、Avro、Parquet、ORC,磁盘上的字节布局完全不同,体积可能差 10 倍,查询速度可能差几十倍。选错格式,再强的引擎也救不回来;选对格式,普通硬件也能跑出惊人的性能。

这份文档要回答几个根本问题:

  • 数据在磁盘上到底有哪几种”摆法”?
  • 为什么有的格式适合写、有的适合读、有的适合分析?
  • CSV、JSON、Avro、Parquet、ORC、Protobuf 各自的定位和取舍是什么?
  • 在一个真实的数据系统里,它们是怎么分工配合的?

读完之后,面对任何”该用什么格式”的问题,你都能从第一性原理推导出答案,而不是死记结论。

分类的第一性原理:三个维度

要看懂所有文件格式,先建立一个三维坐标系。任何一种格式,都可以用这三个维度定位。

维度一:文本 vs 二进制

文本格式(Text-based):数据以人类可读的字符存储(CSV、JSON)。

二进制格式(Binary):数据以紧凑的二进制字节存储(Avro、Parquet、ORC、Protobuf)。

一句话:文本格式是”给人看的”,二进制格式是”给机器用的”。生产环境的大数据系统几乎全用二进制。

维度二:行式 vs 列式

这是大数据格式里最重要的分界线,决定了一个格式适合”写/事务”还是”读/分析”。

行式存储(Row-oriented):一行(一条记录)的所有字段连续存放,一行接一行。

第1行所有字段 | 第2行所有字段 | 第3行所有字段 | ...
1,Alice,25,北京 | 2,Bob,30,上海 | 3,Carol,28,北京

列式存储(Column-oriented):同一列的所有值连续存放,一列接一列。

所有id | 所有name | 所有age | 所有city
1,2,3 | Alice,Bob,Carol | 25,30,28 | 北京,上海,北京

这个区别的影响是决定性的,第二章会专门深入。这里先记住:

维度三:是否自带 Schema、是否支持 Schema 演化

Schema(模式) 就是”这张表有哪些列、每列什么类型”的定义。

Schema 演化(Schema Evolution):业务在变,表结构会改——加字段、删字段、改类型、重命名。一个格式能否在”老数据用老 Schema、新数据用新 Schema”的情况下,依然让新旧数据都能被正确读取,就是 Schema 演化能力。这是流式/长期存储场景的关键能力,Avro 在这方面是公认的标杆。

三维定位表

格式文本/二进制行式/列式Schema
CSV文本行式
JSON文本行式无(自描述但无强类型)
XML文本行式可选(XSD)
Protobuf二进制行式强 Schema,演化好
Avro二进制行式强 Schema,演化最佳
Parquet二进制列式强 Schema
ORC二进制列式强 Schema

记住这张表,下面就是逐个深入。

行式 vs 列式

差异

有一张 100 列、1 亿行的用户行为表。跑一个典型分析查询:

SELECT city, AVG(age) FROM users GROUP BY city;

这个查询只用到 2 列:cityage

行式存储的执行
数据是一行行存的,agecity 散落在每一行里,被其他 98 列包围。要拿到这 2 列,磁盘必须把全部 100 列、1 亿行的数据都读进来,然后从每行里抠出 2 个字段,丢弃其余 98 个。
→ 读了 100 列的数据,用了 2 列。98% 的磁盘 IO 是纯浪费。

列式存储的执行
city 列和 age 列各自连续存在一起。引擎只需读取这 2 个列块,其余 98 列的字节碰都不碰
→ 读了 2 列,用了 2 列。IO 精准,快几十倍。

这就是为什么所有数据仓库、数据湖的分析场景都用列存——分析查询的典型特征就是”宽表、少列、海量行扫描聚合”。

列式的三大红利

红利一:列裁剪(Column Pruning) —— 只读查询涉及的列。表越宽、查询用列越少,优势越大。

红利二:极致压缩 —— 同一列数据类型相同、取值相似、大量重复

红利三:向量化计算(Vectorization) —— 一列同类型数据连续排列,CPU 可用 SIMD 指令一次并行处理一批值。ClickHouse、DuckDB、Spark 的高性能都建立在此之上。

列式的代价

天下没有免费的午餐。列式的短板正是行式的强项:

所以列式不适合 OLTP(在线事务处理:频繁增删改查单条记录)。那是 MySQL、PostgreSQL 这类行式存储引擎的领域。

文本格式:CSV、JSON、XML

CSV —— 最朴素的行式文本

是什么:逗号分隔值,每行一条记录,字段用逗号分隔。

id,name,age,city
1,Alice,25,北京
2,Bob,30,上海

优点

致命缺点

适用:小数据量交换、与非技术人员/Excel 协作、一次性导入导出。绝不适合大规模存储和分析。

JSON —— 灵活的半结构化文本

是什么:键值对结构,天然支持嵌套对象和数组。

{"id": 1, "name": "Alice", "age": 25, "address": {"city": "北京", "zip": "100000"}}

优点

缺点

适用:API 数据交换、配置文件、日志、Schema 多变的半结构化数据。大数据场景里常作为原始接入层(raw) 的格式,之后转成 Parquet 再分析。

衍生:JSON Lines(每行一个 JSON 对象) 常用于日志和流式数据,比标准 JSON 数组更适合逐行追加和处理。

XML —— 重型的标记语言

是什么:用标签嵌套描述数据,可配 XSD 定义 Schema。

现状:极其冗余(开闭标签成对出现)、解析重、性能差。在大数据领域已基本被 JSON 取代,只在传统企业系统、SOAP 接口、特定行业标准里残留。了解即可,新系统不要选它。

行式二进制格式:Avro、Protobuf

文本格式解决不了”小而快”,于是有了行式二进制格式。它们依然按行存(适合写和整行读),但用二进制 + Schema 把体积和速度做上去了。

Avro —— 流式与序列化之王

是什么:Hadoop 生态出身的行式二进制格式,自带 Schema(用 JSON 描述 Schema)。

核心特征:

① 强 Schema + 最佳 Schema 演化能力。 这是 Avro 的招牌。它的读写分离设计:写数据时用”写 Schema”,读数据时可以用不同的”读 Schema”,Avro 自动做兼容匹配。加字段(给默认值)、删字段、改名(用 alias)都能平滑兼容。在长期存储和流式场景里,这个能力价值千金——上游表结构变了,下游不用全部重刷。

② 行式存储,写入高效。 一条记录的所有字段连续序列化,整条写、整条读都快。

③ 紧凑的二进制。 比 JSON/CSV 小很多,且自带类型。

④ Schema 与数据可分离。 在 Kafka 这类场景,消息体只带数据,Schema 存在独立的 Schema Registry 里,进一步减小每条消息的体积。

适用场景(行存的主场):

Protobuf(Protocol Buffers)—— RPC 通信之王

是什么:Google 开源的二进制序列化格式,需预先用 .proto 文件定义 Schema,编译生成各语言的代码。

特征

与 Avro 的区别

还有 Thrift(Facebook 出品),定位类似 Protobuf,了解即可。

列式二进制格式:Parquet、ORC

这是大数据分析的主力。两者都是列式二进制、自带 Schema、带统计信息、支持谓词下推,是真正的”分析格式”。

Parquet

出身:Twitter + Cloudera 联合推动,受 Google Dremel 论文启发。

内部结构

Parquet File
├── Row Group 1   (行组:先把数据横切成若干批行,便于并行)
│   ├── Column Chunk: id     (行组内某列的全部数据)
│   │   ├── Page 1   (页:读写/压缩/编码的最小单元)
│   │   └── Page 2
│   ├── Column Chunk: name
│   └── ...
├── Row Group 2
└── Footer   (文件尾:Schema + 所有 Row Group/Column 的位置和统计信息)

关键设计点:

  1. 先横切成 Row Group,再组内按列。 不是把整列从头存到尾,而是先按行切成 128MB 左右的 Row Group,每个 Row Group 内部再按列存。这样每个 Row Group 能被不同节点并行处理,也能整组跳过。
  2. Footer 在文件尾部。 因为文件顺序写入,写完才知道各部分的最终位置,所以”全局地图”(Schema + 统计信息)放最后。读取时先读 Footer 拿到地图,再精准按需读取。
    • 信息包括完整的 Schema(每列的名字、类型)、每个 Row Group、每个 Column Chunk 的位置(offset)和统计信息(min/max/null count/行数)
    • 为什么元数据放在尾部? 因为 Parquet 文件是一次性顺序写入的。写的时候不知道总共有多少数据、各部分最终在什么位置,只有全部写完才能确定。所以把”全局索引”放在最后写。读取时,引擎先跳到文件末尾读 Footer,拿到整张地图,再精准地按需读取需要的 Row Group 和 Column Chunk。
  3. 内嵌统计信息 + 谓词下推。 每个 Row Group、每个 Page 都存了列的 min/max/null count。查询 WHERE age>50 时,min/max 范围不满足的 Row Group/Page 直接跳过、不读磁盘。这叫 data skipping,是查询飞快的核心。
  4. 编码 + 压缩双层。 先用字典编码/RLE/Delta/位压缩做智能编码,再套 Snappy/ZSTD/Gzip 通用压缩。列存让压缩发挥到极致。
  5. 支持复杂嵌套(struct/list/map),用 Dremel 的 definition/repetition level 把嵌套结构无损压平成列存。

生态地位最广。Spark、Flink、Trino、Presto、Impala、DuckDB、Pandas 全原生支持;Iceberg、Hudi、Delta Lake 三大数据湖表格式默认底层文件都是 Parquet。新项目几乎默认选 Parquet。

谓词下推

光是”按列存”还不够。Parquet 真正的查询加速,来自 谓词下推(Predicate Pushdown) 和它存储的统计信息。

“谓词”就是查询里的过滤条件(WHERE age > 50)。“下推”是指:把这个过滤条件尽可能早地、下沉到存储层去执行,而不是把所有数据读到计算引擎再过滤。

还记得 Footer 和每个 Page 里都存了 min/max 统计信息吗?这就是谓词下推的弹药。

查询 WHERE age > 50 时:

  1. 引擎先读 Footer,看每个 Row Group 的 age 列统计。
  2. Row Group 1 的 age 范围是 min=20, max=45整个 Row Group 都不可能有 age>50 的行,直接跳过,不读!
  3. Row Group 2 的 age 范围是 min=48, max=80 → 可能有,读进来。
  4. 在 Row Group 2 内部,再用 Page 级的 min/max 进一步跳过不相关的 Page。

这叫 数据跳过(Data Skipping)。一张 100GB 的表,可能实际只读了 5GB——其余 95GB 靠统计信息”看一眼元数据就排除了”,根本没碰磁盘上的真实数据。


回想 Iceberg/Hudi/Delta:它们在自己的元数据里也记录了文件级的统计信息,做的是文件级的跳过(这个文件要不要读)。而 Parquet 在文件内部做 Row Group 级、Page 级的跳过。两层跳过叠加:

表格式元数据  →  跳过整个不相关的 Parquet 文件


Parquet Footer  →  跳过文件内不相关的 Row Group


Parquet Page 统计  →  跳过 Row Group 内不相关的 Page

层层过滤,最终只读真正需要的那几 KB。这就是数据湖能在 PB 级数据上做到秒级查询的底层原理之一。

排序的重要性:data skipping 的效果高度依赖数据是否按查询列排序。如果 age 是乱序的,每个 Row Group 的 min/max 范围都是 0~100,那谁都跳不过去。所以入湖时常按高频过滤列排序(Iceberg 的 sort order、Delta 的 Z-Order 就是干这个的)。

编码与压缩

列存的另一大威力是高压缩比。Parquet 分两步压缩:先编码(encoding),再压缩(compression)

编码

编码是利用”同列数据的规律”做的无损紧凑表示,常见几种:

压缩

编码后,再对字节流套一层通用压缩算法:

算法特点适用
Snappy压缩/解压极快,压缩率中等默认首选,平衡之选
ZSTD压缩率高,速度也不错近年流行,存储敏感时优选
Gzip压缩率高,但慢冷数据、归档
LZ4极快,压缩率较低追求解压速度
不压缩极少用

为什么列存能压得这么狠?核心原因再强调一遍:同一列数据,类型相同、取值相似、甚至大量重复

实践中,Parquet 相比同样数据的 CSV,体积常常只有 1/5 到 1/10,甚至更小,同时查询还更快。又小又快,这就是它统治大数据分析的原因。

Schema 与嵌套结构

和 CSV”纯数据、没类型”不同,Parquet 文件自带完整的 Schema(存在 Footer 里)——每列叫什么、是什么类型(int32、int64、float、string、boolean、timestamp…)。读取时无需猜测、无需额外指定,类型安全。

支持复杂嵌套类型

Parquet 不只能存扁平的二维表,还能存嵌套结构(struct、list、map),比如一个字段本身是一个数组或一个对象。它用一套叫 Definition Level(定义层级)和 Repetition Level(重复层级) 的机制(源自 Google Dremel 论文)来把嵌套结构”压平”成列式存储,同时无损还原。

这让 Parquet 能直接表达 JSON 那样的半结构化数据,而又保有列存的全部优势——这是它能成为通用分析格式的重要原因。

ORC —— Hive 生态的列式格式

出身:Hortonworks 为优化 Hive 而生,全称 Optimized Row Columnar。

结构(与 Parquet 异曲同工):

特征:

与 Parquet 的真实对比:

维度ParquetORC
存储方式列式列式
定位通用分析、跨引擎Hive 数仓优化
生态广度最广(Spark/Trino/数据湖默认)Hive/老 Spark 栈深
压缩率常略高
嵌套支持良好
索引列统计 + 可选 bloom filter轻量 row index 强
现状新项目默认既有 Hive 数仓为主

结论:纯分析场景里,Parquet 和 ORC 才是真正的同维度竞品。如果你在 Hive 重度栈里,ORC 可能更优;其他绝大多数情况,选 Parquet(生态最广、最不会错)。

横向总览与选型

全格式对比总表

格式文本/二进制行/列Schema压缩谓词下推主战场
CSV文本Excel/简单交换
JSON文本自描述无强类型API/日志/配置
XML文本可选传统企业/遗留系统
Protobuf二进制RPC/gRPC 通信
Avro二进制强,演化最佳流式/Kafka/CDC
Parquet二进制数据湖/数仓分析
ORC二进制Hive 数仓分析

按场景选型

要和人/Excel 交换小数据 → CSV
API 传输、半结构化、Schema 多变的原始数据 → JSON
微服务间 RPC 通信 → Protobuf(配 gRPC)
Kafka 消息 / CDC 流式传输 / 写密集 / Schema 常演化 → Avro
数据湖 / 数据仓库 / 海量分析查询 → Parquet(首选)或 ORC(Hive 栈)

一条决策主线

要给人看 / 简单交换? ── 是 → CSV / JSON
        │否

是"写多、整行处理、流式传输"? ── 是 → Avro(存储)/ Protobuf(通信)
        │否(读多、分析扫描)

是"海量数据、列式分析"? ── 是 → Parquet(通用首选)/ ORC(Hive 重度栈)

它们在真实系统里如何配合

最重要的认知:这些格式不是”二选一”,而是在一条数据链路的不同环节各司其职。 看一条典型的”数据库实时入湖”链路:

① MySQL (行式存储引擎,OLTP)
        │  binlog 捕获 (CDC)

② 变更事件序列化为 Avro / JSON
        │  写入 Kafka 消息队列(行存、写密集,Avro 主场)

③ 流处理引擎消费 (Flink / InLong Sort)
        │  按表分流、转换、攒批

④ 落盘为 Parquet 文件
        │  组织进数据湖表格式 (Iceberg / Hudi / Delta)

⑤ 查询引擎读取 Parquet (Spark / Trino) 做分析

每个环节的格式选择,都精准对应了该环节的负载特征:

特别注意 Hudi MOR 表的内部分工(前面讲过,这里正好闭环):

这就是文件格式的全貌——没有”最好的格式”,只有”最适合某个环节负载特征的格式”。 行存为写而生,列存为读而生,文本为通用而生,二进制为性能而生。理解了每个维度的取舍,你就能为任何场景选对格式。