大数据文件格式全景:Parquet、ORC、Avro、Protobuf、CSV、JSON 的设计原理与选型指南
- 编程
- 大数据
- 数据工程
目录
- 写在前面:为什么”文件格式”是一个值得专门研究的话题
- 分类的第一性原理:三个维度
- 维度一:文本 vs 二进制
- 维度二:行式 vs 列式
- 维度三:是否自带 Schema、是否支持 Schema 演化
- 三维定位表
- 行式 vs 列式
- 差异
- 列式的三大红利
- 列式的代价
- 文本格式:CSV、JSON、XML
- CSV —— 最朴素的行式文本
- JSON —— 灵活的半结构化文本
- XML —— 重型的标记语言
- 行式二进制格式:Avro、Protobuf
- Avro —— 流式与序列化之王
- Protobuf(Protocol Buffers)—— RPC 通信之王
- 列式二进制格式:Parquet、ORC
- Parquet
- 内部结构
- 谓词下推
- 编码与压缩
- 编码
- 压缩
- Schema 与嵌套结构
- 支持复杂嵌套类型
- ORC —— Hive 生态的列式格式
- 横向总览与选型
- 全格式对比总表
- 按场景选型
- 一条决策主线
- 它们在真实系统里如何配合
写在前面:为什么”文件格式”是一个值得专门研究的话题
很多人对数据的认知停留在”数据就是一张表”。但数据要落到磁盘上、要在网络里传输、要被引擎读取计算,它就必须被序列化成某种字节排列——这就是”文件格式”。
同样一张用户表,存成 CSV、JSON、Avro、Parquet、ORC,磁盘上的字节布局完全不同,体积可能差 10 倍,查询速度可能差几十倍。选错格式,再强的引擎也救不回来;选对格式,普通硬件也能跑出惊人的性能。
这份文档要回答几个根本问题:
- 数据在磁盘上到底有哪几种”摆法”?
- 为什么有的格式适合写、有的适合读、有的适合分析?
- CSV、JSON、Avro、Parquet、ORC、Protobuf 各自的定位和取舍是什么?
- 在一个真实的数据系统里,它们是怎么分工配合的?
读完之后,面对任何”该用什么格式”的问题,你都能从第一性原理推导出答案,而不是死记结论。
分类的第一性原理:三个维度
要看懂所有文件格式,先建立一个三维坐标系。任何一种格式,都可以用这三个维度定位。
维度一:文本 vs 二进制
文本格式(Text-based):数据以人类可读的字符存储(CSV、JSON)。
- 优点:人能直接打开看、任何语言都能解析、调试方便、通用性极强。
- 缺点:体积大(数字
1234567要存 7 个字符 = 7 字节,而二进制 int 只要 4 字节)、解析慢(要把字符串"123"转成数字 123)、没有类型(一切都是字符串)。
二进制格式(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 | 北京,上海,北京
这个区别的影响是决定性的,第二章会专门深入。这里先记住:
- 行式 → 写友好、整行读友好 → OLTP / 流式写入 / 消息传输
- 列式 → 列裁剪、高压缩、分析友好 → OLAP / 数据仓库 / 数据湖
维度三:是否自带 Schema、是否支持 Schema 演化
Schema(模式) 就是”这张表有哪些列、每列什么类型”的定义。
- 无 Schema:CSV——文件里只有数据,没有类型信息,列名靠第一行约定,类型全靠读取方猜。
- 自带 Schema:Avro、Parquet、ORC、Protobuf——文件/消息里携带或关联了完整的类型定义,读取时类型安全、无需猜测。
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 列:city 和 age。
行式存储的执行:
数据是一行行存的,age 和 city 散落在每一行里,被其他 98 列包围。要拿到这 2 列,磁盘必须把全部 100 列、1 亿行的数据都读进来,然后从每行里抠出 2 个字段,丢弃其余 98 个。
→ 读了 100 列的数据,用了 2 列。98% 的磁盘 IO 是纯浪费。
列式存储的执行:
city 列和 age 列各自连续存在一起。引擎只需读取这 2 个列块,其余 98 列的字节碰都不碰。
→ 读了 2 列,用了 2 列。IO 精准,快几十倍。
这就是为什么所有数据仓库、数据湖的分析场景都用列存——分析查询的典型特征就是”宽表、少列、海量行扫描聚合”。
列式的三大红利
红利一:列裁剪(Column Pruning) —— 只读查询涉及的列。表越宽、查询用列越少,优势越大。
红利二:极致压缩 —— 同一列数据类型相同、取值相似、大量重复。
city列全是城市名 → 字典编码 + 游程编码,“北京”出现百万次只存一次。age列全是 0~120 的小整数 → 位压缩,每个值几个 bit 搞定。- 行式存储里
1,Alice,25,北京各种类型混杂,压缩算法无从下手。 - 实测:Parquet/ORC 相比 CSV,体积常常只有 1/5 ~ 1/10。
红利三:向量化计算(Vectorization) —— 一列同类型数据连续排列,CPU 可用 SIMD 指令一次并行处理一批值。ClickHouse、DuckDB、Spark 的高性能都建立在此之上。
列式的代价
天下没有免费的午餐。列式的短板正是行式的强项:
- 写入慢:插入一整行,要把每个字段分别追加到各自的列块里,写操作分散。
- 整行读慢:要取”某条完整记录”,得从每一列里各捞一个值再拼装。
- 不支持高频更新:列式文件通常不可变,改一行代价高。
所以列式不适合 OLTP(在线事务处理:频繁增删改查单条记录)。那是 MySQL、PostgreSQL 这类行式存储引擎的领域。
文本格式:CSV、JSON、XML
CSV —— 最朴素的行式文本
是什么:逗号分隔值,每行一条记录,字段用逗号分隔。
id,name,age,city
1,Alice,25,北京
2,Bob,30,上海
优点:
- 极致通用——Excel、数据库、任何语言、任何工具都能读写。
- 人类可读,调试和肉眼检查方便。
- 追加写简单(直接在文件末尾加一行)。
致命缺点:
- 无类型:所有值都是字符串,
age是数字还是文本?读取方得自己猜和转换。 - 无 Schema:列名靠第一行(还不一定有表头),列的类型完全没有定义。
- 无压缩(裸 CSV):数字以字符存储,体积大。
- 无统计信息:查询必须全文件扫描,没有任何跳过机制。
- 转义地狱:字段里本身有逗号、换行、引号时,转义规则混乱,各家实现不一致,极易出错。
适用:小数据量交换、与非技术人员/Excel 协作、一次性导入导出。绝不适合大规模存储和分析。
JSON —— 灵活的半结构化文本
是什么:键值对结构,天然支持嵌套对象和数组。
{"id": 1, "name": "Alice", "age": 25, "address": {"city": "北京", "zip": "100000"}}
优点:
- 自描述(字段名在数据里)、支持嵌套和复杂结构。
- Web/API 的事实标准,灵活,无需预定义 Schema。
缺点:
- 比 CSV 更冗余(每条记录都重复存所有字段名)。
- 文本解析慢,体积大,无类型强约束。
- 同样无统计、无压缩、不适合分析扫描。
适用: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 里,进一步减小每条消息的体积。
适用场景(行存的主场):
- 消息序列化:Kafka 消息的事实标准格式之一。
- CDC / 流式数据中转:数据库变更事件、流管道的传输格式。
- 写密集、需要整行处理的场景。
- Schema 频繁演化的长期数据。
Protobuf(Protocol Buffers)—— RPC 通信之王
是什么:Google 开源的二进制序列化格式,需预先用 .proto 文件定义 Schema,编译生成各语言的代码。
特征:
- 极致紧凑、极快,强类型、Schema 演化好(字段用编号标识,加减字段兼容性强)。
- 与 gRPC 深度绑定,是微服务间 RPC 通信的主流格式。
与 Avro 的区别:
- Protobuf 更偏向服务间通信/RPC(需要编译、强类型代码绑定)。
- Avro 更偏向大数据存储/流式(动态 Schema、无需编译、与 Hadoop 生态贴合)。
- 大数据存储选 Avro,服务通信选 Protobuf,是常见分工。
还有 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 的位置和统计信息)
关键设计点:
- 先横切成 Row Group,再组内按列。 不是把整列从头存到尾,而是先按行切成 128MB 左右的 Row Group,每个 Row Group 内部再按列存。这样每个 Row Group 能被不同节点并行处理,也能整组跳过。
- Footer 在文件尾部。 因为文件顺序写入,写完才知道各部分的最终位置,所以”全局地图”(Schema + 统计信息)放最后。读取时先读 Footer 拿到地图,再精准按需读取。
- 信息包括完整的 Schema(每列的名字、类型)、每个 Row Group、每个 Column Chunk 的位置(offset)和统计信息(min/max/null count/行数)
- 为什么元数据放在尾部? 因为 Parquet 文件是一次性顺序写入的。写的时候不知道总共有多少数据、各部分最终在什么位置,只有全部写完才能确定。所以把”全局索引”放在最后写。读取时,引擎先跳到文件末尾读 Footer,拿到整张地图,再精准地按需读取需要的 Row Group 和 Column Chunk。
- 内嵌统计信息 + 谓词下推。 每个 Row Group、每个 Page 都存了列的 min/max/null count。查询
WHERE age>50时,min/max 范围不满足的 Row Group/Page 直接跳过、不读磁盘。这叫 data skipping,是查询飞快的核心。 - 编码 + 压缩双层。 先用字典编码/RLE/Delta/位压缩做智能编码,再套 Snappy/ZSTD/Gzip 通用压缩。列存让压缩发挥到极致。
- 支持复杂嵌套(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 时:
- 引擎先读 Footer,看每个 Row Group 的 age 列统计。
- Row Group 1 的 age 范围是
min=20, max=45→ 整个 Row Group 都不可能有 age>50 的行,直接跳过,不读! - Row Group 2 的 age 范围是
min=48, max=80→ 可能有,读进来。 - 在 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)。
编码
编码是利用”同列数据的规律”做的无损紧凑表示,常见几种:
- 字典编码(Dictionary Encoding):列里重复值多时(如 city 列),建一个字典
{0:"北京", 1:"上海"},数据里只存编号[0,1,0]。把长字符串变成小整数,省空间又利于后续压缩。这是 Parquet 默认且最常用的编码。 - RLE 游程编码(Run-Length Encoding):连续相同值,记”值 + 重复次数”。比如
[北京,北京,北京]→(北京, 3)。配合字典编码的整数效果极好。 - Delta 编码:存相邻值的差值而非原值。适合递增的 id、时间戳列——
[1000,1001,1003]→[1000,+1,+2],差值小,存得省。 - Bit-Packing 位压缩:值范围小时用更少的 bit 表示。age(0~127)只需 7 bit,不必用 32 bit 的 int。
压缩
编码后,再对字节流套一层通用压缩算法:
| 算法 | 特点 | 适用 |
|---|---|---|
| Snappy | 压缩/解压极快,压缩率中等 | 默认首选,平衡之选 |
| ZSTD | 压缩率高,速度也不错 | 近年流行,存储敏感时优选 |
| Gzip | 压缩率高,但慢 | 冷数据、归档 |
| LZ4 | 极快,压缩率较低 | 追求解压速度 |
| 不压缩 | — | 极少用 |
为什么列存能压得这么狠?核心原因再强调一遍:同一列数据,类型相同、取值相似、甚至大量重复。
- 行存里
1,Alice,25,北京这一行,整数、字符串、年龄、城市混在一起,压缩算法面对”杂乱的字节”无能为力。 - 列存里
[北京,上海,北京,北京,深圳,北京...]全是城市名,字典编码 + RLE + 通用压缩三连击,能压到原大小的零头。
实践中,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 异曲同工):
- 数据先分成 Stripe(类似 Parquet 的 Row Group,默认约 250MB)。
- 每个 Stripe 内部按列存,分为 index data、row data、stripe footer。
- 文件级有 File Footer 和 Postscript 存元数据和统计。
特征:
- 同为列式,同样支持谓词下推、压缩、列裁剪。
- 压缩率通常略高于 Parquet(尤其在 Hive 典型数据上)。
- 轻量级索引(row index)做得强,Stripe 内部可以更细粒度跳过。
- 支持 ACID(在 Hive 事务表场景)。
与 Parquet 的真实对比:
| 维度 | Parquet | ORC |
|---|---|---|
| 存储方式 | 列式 | 列式 |
| 定位 | 通用分析、跨引擎 | 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) 做分析
每个环节的格式选择,都精准对应了该环节的负载特征:
- ① OLTP 层:MySQL 用行存——要频繁增删改查单条记录。
- ② / ③ 传输层:用 Avro(或 JSON)——流式、写密集、要 Schema 演化,行存正合适。
- ④ / ⑤ 分析层:用 Parquet——海量数据列式分析,列存正合适。
特别注意 Hudi MOR 表的内部分工(前面讲过,这里正好闭环):
- 增量写入先落 Avro 格式的 log file(行存,写得快)。
- 后台 compaction 再合并成 Parquet 格式的 base file(列存,查得快)。
- 同一张表内部,行存负责写、列存负责读,两种格式协同。
这就是文件格式的全貌——没有”最好的格式”,只有”最适合某个环节负载特征的格式”。 行存为写而生,列存为读而生,文本为通用而生,二进制为性能而生。理解了每个维度的取舍,你就能为任何场景选对格式。