小王算钱

Interview Prep · Data Infra

大数据文件格式:
从 CSV 到 Lance

这门课按时间线讲每一种格式:它要解决上一种的什么问题,字节怎么放,读和写各走哪几步,优缺点,现在还在哪里用。后面有对比表、Staff 级的面试题、一个能跑的练习和一页速查表。

时间线

从文本文件到 Lance,每一步在解决上一步的什么问题

  1. 出处

    Twitter 和 Cloudera

    它要解决的问题

    Twitter 想要一个不绑定任何查询引擎的列存格式,并且能存 Thrift 那样的嵌套数据。Cloudera 的 Impala 也需要一个列存格式。
    Parquet 的结构,从文件开头到结尾
    • 魔数 PAR1
    • row group
      • 第 1 列的 column chunk
        • 字典 page可选
        • 数据 page默认上限 1 MB,独立编码和压缩
        • 数据 page …
      • 第 2 列的 column chunk
      • 第 3 列 …
    • row group …
    • 文件尾(footer)schema,每个 row group 和 column chunk 的位置,每列的统计信息
    • 文件尾的长度 + 魔数 PAR1

    怎么存

    • 文件切成 row group,每个 row group 里每列是一个 column chunk,column chunk 再切成 page。page 的默认上限是未压缩 1 MB,实际常常更小。
    • page 是编码和压缩的最小单位。常见编码有字典编码、游程编码、增量编码,编码之后再套一层通用压缩。
    • 文件尾(footer)用 Thrift 编码,记录 schema、每个 row group 的位置,以及每列的最小值、最大值、空值数量。
    • 嵌套数据用 Dremel 的 repetition level 和 definition level。

    写入的过程

    1. 攒够一个 row group 的数据,每列编码压缩成若干 page 依次写出。
    2. 所有 row group 写完后,把文件尾写在最后。
    3. 文件尾在末尾,所以文件写完之前不能读。

    读取的过程

    1. 先读文件最后 8 个字节,得到文件尾的长度。
    2. 再读文件尾,拿到 schema 和统计信息。
    3. 根据查询条件挑出需要的 row group 和列,只读这些 column chunk,逐个 page 解压解码。

    优点

    • 只读需要的列。
    • 压缩率高。
    • 靠统计信息跳过整个 row group。
    • 支持嵌套数据。
    • 几乎所有引擎和语言都能读。

    缺点

    • 随机读一行很贵:要读并解码整个 page。文件尾通常有缓存,但定位到 page 靠的 offset index 是可选的,没有它就得顺着 column chunk 一个个 page 头找过去。
    • 图片、向量这类很大的值不好放:row group 按行数切,行数少了整数列太碎,行数多了图片列一组就有几个 GB。
    • 列非常多(几千列)时,文件尾必须整个解析,光读元数据就很慢。
    • 给已有的行补一列的值,需要重写文件。

    用在哪里,现在还在哪里用

    数据湖里的分析查询,以及 Iceberg、Delta、Hudi 的数据文件。是现在的默认选择。

    被什么取代,为什么

    没有被取代。新格式是在它不擅长的地方挑战它:随机读、很宽的值、几千列的表。

    面试时,一句话

    列存的事实标准:行组、列块、页三层结构,文件尾存元数据。

    面试时,两分钟

    Parquet 的结构是三层:文件切成 row group,每个 row group 里每列是一个 column chunk,再切成 page。page 是编码和压缩的单位。文件尾存 schema 和每列的最小值最大值,所以读的时候可以只读需要的列,并跳过整个 row group。嵌套数据用 Dremel 的两个 level。它的短板是随机取一行很贵,很宽的值不好处理,给已有的行补一列要重写。它能成为标准主要靠生态:不绑定引擎,Spark 选它做默认,后来 Arrow、云数仓和表格式都围着它。

    值得补一句的

    它胜出主要靠生态,不是靠技术碾压。下面「Parquet 为什么几乎一统江湖」一节专门讲。

先懂这五个概念

所有格式都在这五件事上做取舍

行存和列存
一条记录的字段挨着放,还是同一列的值挨着放。
写入和整行读取用行存;只读几列的分析查询用列存。这是所有格式的第一个分岔口。
可切分(splittable)
一个大文件能不能从中间切开,让多个任务同时读。
不能切分的文件只能由一个任务读完。sync marker、row group、stripe 都是为了让文件能切开。
编码和压缩
编码利用数据本身的规律(重复、递增),压缩是在字节层面再压一次。
列存压缩率高,是因为同一列的值类型相同、经常重复,先做字典编码或游程编码,再压缩,效果远好于直接压缩一整行。
数据跳过
引擎把查询条件交给读取方(这一步叫谓词下推),读取方靠每块数据的最小值、最大值,跳过不可能命中的块。
查询快不快,很大程度取决于能少读多少数据。统计信息记得越细,能跳过的越多,但元数据也越大。
schema 演进
加字段、删字段、改类型之后,旧数据还能不能读。
数据会存很多年,表结构一定会变。Avro 靠两份 schema 对齐,Protobuf 靠字段编号,Iceberg 给每列一个不变的 ID。

展开讲 · 一

SequenceFile 的一个例子

假设 HDFS 上有一百万张小图片。每个文件都要占 NameNode 的内存,每个文件还会单独起一个读取任务。常见的做法是把它们打包进一个 SequenceFile:key 是文件名,value 是图片的字节。

# PySpark:把很多小文件打包成一个 SequenceFile
# key 是文件名,value 是文件内容
pairs = sc.parallelize([
    ("cat-001.jpg", bytearray(open("cat-001.jpg", "rb").read())),
    ("cat-002.jpg", bytearray(open("cat-002.jpg", "rb").read())),
])
pairs.saveAsSequenceFile("hdfs:///datasets/cats.seq")

# 读回来:得到 (文件名, 内容) 的键值对
sc.sequenceFile("hdfs:///datasets/cats.seq").take(1)

写出来的文件,里面大致是这样:

SEQ\x06                                   魔数和版本
org.apache.hadoop.io.Text                 key 的类
org.apache.hadoop.io.BytesWritable        value 的类
压缩: 否   块压缩: 否                      压缩标志
<16 字节 sync marker>                     文件头结束

[记录长度][key 长度] "cat-001.jpg" <图片字节>
[记录长度][key 长度] "cat-002.jpg" <图片字节>
<16 字节 sync marker>                     每隔一段出现一次
[记录长度][key 长度] "cat-003.jpg" <图片字节>

文件头里写的是 Java 的类名,这就是它难以被其他语言读取的原因。sync marker 让一个任务可以从文件中间开始,找到下一条完整的记录。

展开讲 · 二

Parquet 为什么几乎一统江湖

不是因为它技术上碾压了 ORC。两者同一年出现,结构相似,性能各有胜负。差别在生态:

  1. 1

    不绑定任何一个引擎

    ORC 长在 Hive 里。Parquet 由 Twitter 和 Cloudera 一起做,一开始的目标就是让多个引擎都能用,还提供了和 Avro、Thrift、Protocol Buffers 这些数据模型的转换。

  2. 2

    Spark 把它当默认格式

    Spark 的默认数据源是 Parquet。Spark 成为主流计算引擎的那几年,大量新数据自然就写成了 Parquet。

  3. 3

    嵌套数据处理得好

    它用的是 Dremel 的做法,和 Protocol Buffers、Thrift 这类嵌套的数据模型对得上。

  4. 4

    Arrow 和 Python 生态

    pandas、DuckDB、Polars 都能一行代码读写 Parquet。数据科学家不需要搭 Hadoop 也能用它。

  5. 5

    云数仓和表格式都围着它

    各家云上的查询服务都能直接读 Parquet。Delta Lake 只用 Parquet,Iceberg 的默认数据文件也是 Parquet。

一个格式一旦成为大家都能读的那个,新工具就会优先支持它,于是更多数据写成它。这是网络效应,和格式本身的细节关系不大。

展开讲 · 三

Lance 到底改了什么

  1. Parquet 取一行为什么慢

    要取第 N 行的一个值,得先读文件尾,找到它在哪个 row group、哪个 page,再把整个 page(默认上限 1 MB)读出来解压解码,最后取出那一个值。要几列就重复几次。定位 page 靠的 offset index 还是可选的,没有它就得顺着 page 头一个个找。顺序扫描时这些开销被摊薄了,随机读时每一行都要付一遍。

  2. Lance 怎么让随机读变快

    它去掉了 row group,每列自己切 page。2.1 版按值的宽度选存法:整数、短字符串这类窄值切成小块,每块默认最多 4,096 个值,规范要求压缩后不超过 32 KiB,取一个值只需要读一小块;图片、向量这类宽值一个挨一个放,可以直接定位到那个值。分界线在当前规范里是每个值 256 字节。

  3. 加列为什么不用重写

    表由 fragment 组成,一个 fragment 可以有多个数据文件,各存不同的列。给表加 embedding 列时,只需要给每个 fragment 写一个只含新列的文件,再提交一个新版本。原来存着图片的文件完全不动。

  4. 它同时是表格式

    每次写入产生一个新的 manifest,记录这个版本有哪些 fragment。所以它自带历史版本。删除是写一个 deletion file 做标记。向量索引、标量索引和全文索引也记在表里。

  5. 哪些地方要小心

    生态比 Parquet 小得多;格式在一两年内连续出了 2.0、2.1、2.2;版本和小文件多了要定期合并清理。LanceDB 公布过和 Parquet 对比的性能数字,但我没有核对到它们的测试条件,也没有找到独立的验证,所以这里不引用。另外,Lance 的论文自己也提到,Parquet 配置得当时随机读可以比默认设置快很多。

七个追问

面试官顺着问下来,多半是这几句

1gzip 整体压缩的文件,为什么不能切分?

gzip 用的 DEFLATE 算法会写「往回 300 字节,照抄 20 字节」这样的指令,最远可以往回 32 KB。从文件中间开始读的任务手里没有前面那段数据,这条指令就没法执行。另外,DEFLATE 内部的块可以从任意一个 bit 开始,中间没有可以搜索的标记;每个块的编码表又写在块的开头。三件事加起来,只能从头读到尾。

上:整个文件压成一条 gzip 流。下:容器格式在内部一块一块地压缩。红线是切分点。
前面的数据
← 往回 300 字节,照抄 20 字节

从红线开始读的任务:要抄的那 20 字节在红线左边,我没有。

块 1,独立压缩
S
块 2
块 2
S
块 3,独立压缩

从红线开始读的任务:往后找到下一个 S,从块 3 开始解压。块 2 归前一个任务。

能切分的压缩有两类。一类是 bzip2:每块独立压缩,块头有固定的标记,可以从中间搜到下一块。另一类是容器格式在内部分块压缩,SequenceFile、Avro、Parquet 都是这样。

2SequenceFile 从中间开始读,sync marker 之前的记录怎么办?

不是丢掉,是归上一个任务。规则有两条,配合着用:每个任务先往后找到自己这段里的第一个 sync marker,从它后面开始读;读到这段的末尾也不马上停,要继续读到越过末尾后的第一个 sync marker。这样每条记录恰好被读一次。

「记」是一条记录,S 是 sync marker,红线是两个任务的分界。
头
记
记
S
记
记
记
S
记
记
S
记
任务 A 读这些
—
任务 B 读这些

任务 A 读过红线,直到碰到下一个 S 才停。红线后面紧跟的那条记录在任务 B 的范围里,但由任务 A 读;任务 B 跳过它,从 S 之后开始。

3SequenceFile 为什么存成键值对?它又不是哈希表。

它确实不是哈希表:不能按 key 查找,key 也可以重复,只是「一串键值对」。存成这样是因为 MapReduce 的每一步都是键值对,一个作业的输出要能直接当下一个作业的输入。

用数单词的例子看 MapReduce 的数据怎么流动。每一步进出的都是键值对。
  1. 输入

    (0, "a b a")

  2. map 输出

    (a, 1) (b, 1) (a, 1)

  3. 按 key 分组

    (a, [1, 1]) (b, [1])

  4. reduce 输出

    (a, 2) (b, 1)

很多时候 key 没有实际意义。存一张表时,常见做法是 key 留空,把整行放进 value。真要按 key 查找,Hadoop 另有 MapFile:排好序的 SequenceFile 加一个索引。

4RCFile 当时为什么不带类型?

因为它把类型留给了上一层。RCFile 是为 Hive 写的,Hive 当时的分工是三层,RCFile 只做最下面那一层。论文给它定的目标也是摆放结构:加载快、查询快、省空间、能适应变化的查询。

Hive 读一张 RCFile 表时的三层分工。类型在上面两层,文件里只有字节。
  1. metastore:表的 schema

    name string, age int

  2. SerDe:把字节解释成类型

    字节 → "alice"、30

  3. RCFile:只管字节怎么摆

    每列是一段压缩过的字节

代价后来才显出来:文件不知道类型,就没法给整数用增量编码、给字符串用字典编码,也记不了最小值和最大值。ORC 把类型放进了文件,修的就是这一点。这一段的三层分工是按 Hive 当时的架构说的,我没有在论文里找到「为什么不存类型」的直接说明。

5ORC 和 Parquet 存嵌套数据,具体差在哪?

用同一份数据看。schema 是 struct<name: string, tags: array<string>>,有三行:

行nametags
1a[x, y]
2b[](空数组)
3cnull

ORC 把结构里的每一层都当成一列,包括中间的 list。每列有几个流:PRESENT 标记每个值是不是空,LENGTH 记每一行有几个元素(或者字符串多长),DATA 是值本身。

ORC:三列,每列各有自己的流。父节点为空时,子节点那里什么都不存。
列PRESENTLENGTHDATA
name1, 1, 11, 1, 1abc
tags(list 这一层)1, 1, 02, 0—
tags 的元素1, 11, 1xy

Parquet 只存叶子列,list 这一层没有自己的列。结构信息全部放进叶子列的两个 level 里。

Parquet:「tags 的元素」这一个叶子列。没有值的地方也要占一行,用 level 说明原因。
值repetition leveldefinition level含义
x03新的一行,值存在
y13还在同一个 list 里
—01新的一行,list 存在但是空的
—00新的一行,list 本身是 null

取舍:ORC 直观,但读一个嵌得很深的叶子,要把路径上每一层的 PRESENT 和 LENGTH 都读出来。Parquet 只读一列就能还原结构,但每个叶子都要带两个 level,拼回记录的逻辑更复杂。

6字典编码、游程编码、增量编码,各举一个例子

字典编码:取值种类少的列。每个值换成一个很小的编号。
原始
USCNUSUSINCN
字典
0 = US1 = CN2 = IN
编码后
010021
游程编码:相同的值连在一起的列。只记「什么值,连续几个」。
原始
okokokokokfailfailok
编码后
ok × 5fail × 2ok × 1
增量编码:慢慢变大的数字。只记第一个值和后面每一步的差。
原始
1700000000170000000317000000041700000009
编码后
1700000000+3+1+5

三种可以叠着用。Parquet 常见的做法是先字典编码,再对编号做游程编码或者按 bit 紧凑存放。游程编码在数据排过序时效果最好,值交替出现时没有用,下面动手练习里的 country 列就是后一种情况。

7没有 Arrow 的时候,Spark 把数据交给 Python 有多慢?

假设 Spark 里有一千万行数字,你想用 Python 给每个数加 1。计算本身放在 NumPy 里只要几十毫秒,时间几乎全花在搬运上。

没有 Arrow:下面这一圈,每一行都要走一遍,一共一千万次。
  1. JVM:把这一行从自己的内存格式转成 pickle 字节

  2. 通过 socket 发给 Python 进程

  3. Python:把字节还原成一个 Python 对象

  4. 调用你的函数,算出 x + 1

  5. 结果再 pickle 一次发回去,JVM 转回自己的格式

有 Arrow:一次传一批(比如一万行),两边用的是同一种内存布局。
  1. JVM:把一批数据写成 Arrow 的列式布局

  2. Python:拿到的就是 pandas 能直接用的数组,不用逐个转换

  3. 对整个数组算 x + 1,结果按同样的布局传回去

代码上的差别只是把 @udf 换成 @pandas_udf,函数的参数从一个数变成一个 pandas.Series。Databricks 在 2017 年发布这个功能时,用一千万行数据测了三个函数,结果是比逐行的 UDF 快 3 倍到 100 倍以上。这是 Databricks 自己的测试。

横向对比

同样的几个维度,放在一起看

格式行还是列schema 在哪能否切分能否跳过数据最适合
CSV / JSON行,文本没有不压缩时基本可以不能交换数据、人工查看
SequenceFile行,二进制只有 Java 类名可以不能历史系统、打包小文件
Avro行,二进制在文件头里可以不能Kafka、数据采集、元数据文件
Thrift / Protobuf单条消息,不是文件格式在 IDL 文件里,数据带字段编号不适用不适用RPC、消息、其他格式的元数据
RCFile列,按 row group没有类型信息可以,按 row group不能,只能不读某些列遗留的 Hive 表
ORC列在文件尾里可以,按 stripe能,默认每 1 万行一条索引Hive 体系的数仓
Parquet列在文件尾里可以,按 row group能数据湖里的分析查询
Arrow列,在内存里随数据一起传不适用不适用系统之间传数据、内存计算
Iceberg / Delta / Hudi表格式,数据文件多为 Parquet在表的元数据里按数据文件能,先跳过整个文件数据湖上的数仓
Lance列,没有 row group在 manifest 和文件尾里可以,按 fragment能,另有索引训练数据、向量检索、多模态数据

业界最新的方向 · 2026 年 10 月核对

现在大家在做什么

  • 为 AI 数据设计的格式

    训练和检索需要随机读,数据里有图片和向量。Lance 是这个方向上最受关注的一个,2026 年初 Apache Polaris 这个 catalog 也接入了 Lance 表。

  • 把编码从文件结构里拆出来

    Nimble、Vortex 和 F3 都让编码可以替换和嵌套。Vortex 在 2025 年 8 月进入 Linux 基金会旗下的 LF AI & Data。

  • Parquet 自己也在更新

    2025 年 3 月的 parquet-format 2.11.0 加入了 GEOMETRY 和 GEOGRAPHY 两种地理类型,也第一次写进了 Variant;2025 年 8 月的 2.12.0 把 Variant 的规范定稿。Variant 用来存结构不固定的半结构化数据。

  • 表格式在补行级更新的能力

    Iceberg 的 v3 规范引入了 deletion vector:用一个位图标记数据文件里哪些行被删了,位图存在 Puffin 文件里。这让频繁的更新和删除更便宜。

常见误解

这几句话听起来对,其实不对

  • 误解:Parquet 胜出是因为技术比 ORC 好。

    两者技术上各有胜负。一篇发表在 VLDB 2024 的论文发现 Parquet 解码更快、文件略小,ORC 跳过数据更有效。Parquet 胜出主要是因为不绑定引擎,以及 Spark 把它当默认格式。

  • 误解:Avro 已经没人用了。

    它退出的只是分析存储。Kafka 消息、数据采集,以及 Iceberg 的 manifest 文件都还在用 Avro。

  • 误解:Iceberg、Delta、Hudi 是文件格式。

    它们是表格式,是数据文件之上的一层元数据。数据文件绝大多数是 Parquet:Delta 只用 Parquet,Iceberg 还允许 ORC 和 Avro。

  • 误解:列存总是比行存好。

    逐条写入、整行读取、按主键取一行,这些场景行存更合适。

  • 误解:Arrow 是 Parquet 的替代品。

    一个管内存,一个管硬盘,通常搭配使用。

  • 误解:压缩过的文件都不能切分。

    把整个文件用 gzip 压成一个流才不能切分。SequenceFile、Avro、Parquet 都是在文件内部一块一块地压缩,所以仍然可以切分。

  • 误解:Lance 会取代 Parquet。

    它们针对的读法不同。全表扫描和聚合仍然是 Parquet 的主场。

Staff 级面试题

先自己答,再点开看

▶为什么列存的压缩率比行存高?
  • 同一列的值类型相同,而且经常重复或者有规律。可以先做编码:重复值多用字典编码,连续相同用游程编码,递增的数字用增量编码。
  • 编码之后再做通用压缩。行存里一行混着整数、字符串、时间,相邻字节之间没有规律,只能直接压缩。
  • 压缩率高多少取决于数据:取值少、有顺序的列收益最大,每行都不同的随机字符串几乎没有收益。
▶一个 10 GB 的 gzip 压缩 CSV 和一个 10 GB 的 Parquet 文件,读起来差在哪?
  • gzip 的 CSV 不能切分,只能由一个任务从头解压到尾。Parquet 可以按 row group 分给多个任务。
  • CSV 必须读全部列并解析文本。Parquet 只读用到的列,还能靠统计信息跳过整个 row group。
  • 差多少取决于查询:只读几列、带过滤条件时差距最大;要读全部列的全表导出,差距就小得多。
▶Kafka 到数据湖的链路上,每一层用什么格式,为什么?
  • Kafka 里用 Avro(或 Protobuf)加 Schema Registry:一条条写入,消息小,schema 经常变。
  • 落到数据湖用 Parquet:之后都是分析查询,只读几列。
  • Parquet 文件上面加 Iceberg 或 Delta:保证写入是原子的,能改表结构,能查历史版本。
▶Iceberg 的数据文件用 Parquet,为什么 manifest 用 Avro?
  • manifest 是一条条「文件记录」,早期字段不多,读的时候基本整条都要用,行存合适。
  • 它需要可靠的 schema 演进,Iceberg 自己升级时会给 manifest 加字段。
  • manifest 写出去就不再修改,行存一次写完很简单。这是当年的取舍:统计信息越来越宽以后,只想读分区和几列统计也得解码整条,行存的缺点就出来了。Iceberg 社区在讨论 v4 时提出过把 manifest 改成 Parquet。
▶row group 应该设多大?
  • 大一些:压缩率更好,元数据更少,顺序扫描更快。
  • 小一些:统计信息更细,能跳过更多数据,读的时候占的内存也更少。
  • 还要考虑并行度:一个 row group 通常是一个读取任务的最小单位。没有标准答案,要说清楚取舍。
▶什么情况下 Parquet 不合适?
  • 按主键取少数几行:每次都要读并解码整个 page。
  • 列里有图片、向量这样很宽的值:row group 大小没法兼顾。
  • 几千列的宽表:文件尾必须整个解析。
  • 经常加列并回填:每次都要重写数据文件。
▶给一张 10 亿行的表加一个 embedding 列,Parquet 加 Iceberg 和 Lance 各要做什么?
  • Parquet 加 Iceberg:改表结构本身很快,只改元数据。Iceberg v3 还允许新列带一个固定的默认值,不用重写。但每一行的值都不同时,要把值填进去就得重写所有数据文件,原有的列也跟着重写一遍。
  • Lance:给每个 fragment 写一个只含新列的数据文件,再提交一个新版本。原来的文件不动。
  • 代价是 Lance 的一个 fragment 会有多个文件,读整行时要多打开几个文件,之后可能需要合并。
  • 还有第三条路:把 embedding 单独存一张表,用主键和原表关联。选哪条取决于这一列多久重算一次,以及查询是不是总要和原表一起读。
▶小文件问题是什么,怎么解决?
  • 流式写入或者分区切得太细,会产生大量很小的文件。HDFS 上每个文件都占 NameNode 的内存;对象存储上,列出文件和逐个打开文件的开销会超过读数据本身。
  • 解决办法是定期合并(compaction)。表格式让合并可以安全地在后台做,因为读的人看到的始终是某个完整的版本。
  • SequenceFile 当年的一个用途就是把小文件打包成大文件。
  • 多久合并一次是取舍:合并得勤,读得快,但合并本身要花计算资源,还会和正在写入的任务冲突。取决于读和写哪边更重要。
▶设计题:给机器学习训练平台选存储格式,你怎么想?
  • 先问读的方式:是整表顺序扫,还是随机取样?有没有图片、视频、向量?特征列多久加一次?
  • 也问周边:现有的引擎和工具能不能读?团队能不能承担一个较新格式的维护风险?
  • 以顺序扫描的表格特征为主,Parquet 加表格式是稳妥的选择。随机访问、多模态、频繁加列占主导时,才值得考虑 Lance 这样的格式。
  • 可以分层:原始数据和报表留在 Parquet,训练用的数据集另存一份。

动手练习 · 约 5 分钟

亲手看一眼 Parquet 的文件尾

需要 Python 和 pyarrow(pip install pyarrow)。把下面的代码存成 demo.py 再运行。

import os
import pyarrow as pa
import pyarrow.csv as pacsv
import pyarrow.parquet as pq

# 1. 造一张 100 万行的表:一列递增的整数,一列只有 4 种取值,一列小数
n = 1_000_000
table = pa.table({
    "user_id": pa.array(range(n), pa.int64()),
    "country": pa.array(["US", "CN", "IN", "BR"] * (n // 4)),
    "amount": pa.array([(i * 7919) % 100_000 / 100 for i in range(n)]),
})

# 2. 分别写成 CSV 和 Parquet,比一比大小
pacsv.write_csv(table, "demo.csv")
pq.write_table(table, "demo.parquet", row_group_size=250_000)
print("CSV    ", os.path.getsize("demo.csv") // 1024, "KB")
print("Parquet", os.path.getsize("demo.parquet") // 1024, "KB")

# 3. 读文件尾(footer):不读数据,就能知道文件的结构
meta = pq.ParquetFile("demo.parquet").metadata
print("row group 数:", meta.num_row_groups, " 总行数:", meta.num_rows)

# 4. 看每一列在第一个 row group 里占多少字节、用了什么编码
for i in range(meta.num_columns):
    col = meta.row_group(0).column(i)
    print(col.path_in_schema, col.total_compressed_size // 1024, "KB", col.encodings)

# 5. 看每个 row group 里 user_id 的最小值和最大值
for g in range(meta.num_row_groups):
    stats = meta.row_group(g).column(0).statistics
    print("row group", g, "user_id:", stats.min, "到", stats.max)

# 6. 只读一列,并且只要 user_id >= 900000 的行
result = pq.read_table(
    "demo.parquet", columns=["amount"], filters=[("user_id", ">=", 900_000)]
)
print("读到", result.num_rows, "行")

在 pyarrow 19.0 上的实际输出:

CSV     18221 KB
Parquet 8991 KB
row group 数: 4  总行数: 1000000
user_id 1249 KB ('PLAIN', 'RLE', 'RLE_DICTIONARY')
country 2 KB ('PLAIN', 'RLE', 'RLE_DICTIONARY')
amount 994 KB ('PLAIN', 'RLE', 'RLE_DICTIONARY')
row group 0 user_id: 0 到 249999
row group 1 user_id: 250000 到 499999
row group 2 user_id: 500000 到 749999
row group 3 user_id: 750000 到 999999
读到 100000 行
  • country 列在第一个 row group 里有 25 万个值,只占 3,049 字节(输出里按 KB 取整,显示成 2 KB)。它只有 4 种取值,字典编码把每个值变成 2 bit 的编号,25 万个值约 62 KB;这些编号按固定顺序循环,Snappy 再把它压到约 3 KB。这里游程编码没起作用,因为相邻的值都不相同。
  • 想看游程编码的效果,把 country 这一列排序后再写一次:相同的值连在一起,这一列会小到几十个字节。
  • 第 5 步打印的最小值和最大值存在文件尾里。第 6 步的过滤条件是 user_id >= 900000,前三个 row group 的最大值都不到 900000,所以整组被跳过,只读了第四组。
  • 自己改一改:把 row_group_size 改成 50000 再跑,看 row group 数和文件大小怎么变;把 country 换成每行都不同的字符串,看那一列变成多大。

速查表

考前十分钟过一遍

CSV / JSON
文本,通用,没有类型,整体压缩后不能切分。
SequenceFile
二进制键值对,sync marker 让它可切分,绑死 Java。
Thrift / Protobuf
不是文件格式。字段编号带来 schema 演进。
Avro
行存,schema 在文件头。写入侧主流:Kafka、Iceberg manifest。
Dremel
repetition level 和 definition level,嵌套数据按列存。
RCFile
Record Columnar:先分行组,组内按列。不认识类型,没有索引。
ORC
stripe 加索引加文件尾,认识类型。Hive 体系。
Parquet
row group、column chunk、page 三层,文件尾存统计。事实标准。
Arrow
内存里的列式布局,零拷贝。和 Parquet 搭配。
Iceberg / Delta / Hudi
表格式:原子提交、历史版本、改表结构。
Lance
没有 row group,随机读快,加列不重写,自带索引。
Nimble / Vortex / F3
宽表、可扩展编码、GPU。都还新。

来源

新格式的部分(Lance、Nimble、Vortex、F3,以及 Parquet 和 Iceberg 的新特性)在 2026 年 10 月核对过,之后可能已经变化。

留言

说说你的看法

有想法或问题都可以写在这里。留言会立刻显示;想收到回复通知再填邮箱。

还没有留言,你可以写第一条。