TT Lab
开始
学习 学习路径 课程

数据流水线

分区键决定了之后的每一次查询

在 TT Lab 中继续学习

一句话总结

分区键选什么,决定了今后所有查询的成本;切得越碎,小文件就堆得越多;选错了之后想改回来,代价是把全部数据重写一遍。

为什么需要它

插入的是同样的行,有的查询 0.2 秒就结束,有的却要 40 秒。查询一样,数据也一样。不同的只有一点:文件在磁盘上是怎么摆放的。

分区就是按值把目录分开摆放。如果 day=2026-01-03/ 下面只有那一天的数据,那么只看那一天的查询就完全不会打开其余目录。没有被打开的文件,成本为 0。这就是分区带来的几乎唯一、而且非常大的好处。

问题在于,这个好处只对作用在键上的条件才会出现。如果按目录名里没有的字段来过滤,就没有可以裁剪的东西,必须全部打开看一遍。所以选键这件事,选的不是数据的性质,而是今后会进来的查询的形状。

工作原理

“那把常用的字段都放进键里不就行了”,这是第一个陷阱。如果把日期、地区和来源渠道都设为键,分区数就是三个值的乘积。每天 300 条的数据一下子散到 160 个目录里,一个文件只有两三行。

把同一天的 300 条数据按两种分区键摆开的示意图。键只有日期一个时,一个目录里放一个 300 行的文件;把键设为日期、地区和渠道三个时,同样的 300 条会散到 160 个目录里,一个文件只有两三行

这就是小文件问题(small files problem)。损失出在三个地方。打开一个文件的固定开销超过了读取内容的开销;管理文件列表的一方(无论是 manifest 清单还是 metastore 元存储)的条目数暴涨;压缩和编码也起不到应有的效果。

看一看一个文件如何与读取单位咬合,就能清楚为什么小文件吃亏。Parquet 的概念文档把行组(row group)定义为“数据按行方向做出的逻辑划分”,把列块(column chunk)定义为“位于特定行组内、保证在文件中连续存放的某一列的数据”。关键就在“连续”二字——这意味着可以通过一次大的顺序读取把它取回来。页(page)是再下一级的单位,是“从压缩和编码的角度看无法再继续拆分的单位”。

Parquet 配置文档建议把行组设得大一些,从 512MB 到 1GB。这是为了获得大的顺序输入输出和大的列块,并把让一个行组恰好放进一个块里视为理想的布局。页则建议 8KB,因为越小,只找一行来读就越容易。

由此得出结论。如果文件远小于行组,这套结构就什么也起不了作用。一个 2KB 的文件里不可能装下 512MB 的行组。读取方只是在每个文件上重复读取末尾的元数据、再打开文件。所以文件大小不是口味问题,而是与读取单位的咬合。

本实验不使用 Parquet。实验镜像里没有 pyarrow,而且 Pod 不能在运行时安装软件包。所以用分区目录、清单和 JSON Lines 手工搭出同样的结构。只是没有“行组”这个名字而已,“一个文件要与读取单位咬合”这一点完全一样。

文件合并与合并过程中的读取方

小文件堆起来之后,就要执行文件合并(compaction)。把一个分区里的小文件首尾相接,合成接近目标大小的大文件。这并不难。难的是合并过程中,读取的人会看到什么。

写新文件的时候,旧文件也还在。这时通过遍历目录来收集文件的读取方,会把旧文件和新文件都捡走,同一行被统计两次。合计恰好变成两倍的事故就出在这里。

防止的办法只有一个。读取方看的是列表(清单),而不是目录。文件合并在写完新文件之后,一次性换上新的列表,然后才删除旧文件。只要列表的替换是原子的,读取方看到的就是完整的旧列表或完整的新列表,中间状态不存在。

新文件的名字也有规则。不能与旧名字重复。一旦重复,就会覆盖正在读取的文件,这个分区就会整个变空。

在现场相遇的样子

第一,改键的代价是全部重写。分区键本身就是目录结构,要改它,就得读取所有行并重新写出所有文件。所以选键的会议值得多开一次。

第二,时间键几乎总是要放进去。因为大部分查询是按时间段进来的,而整体删除旧数据也是以目录为单位的。

第三,值很多的字段不要当键用。如果把用户 ID 或订单号设为键,分区的数量就和行数一样多。这种错误在数据少的时候没有任何症状,几个月之后才暴露出来。

第四,平均文件大小会说谎。一个大文件和几百个小文件混在一起,平均值看上去完好无损。要同时看中位数和低于阈值的文件个数,才能看出真实的样子。

实际工作中真正重要的事

下一项实验要做什么

生成订单原始数据,再一步步扩充工具 pq.py。分别建立一个只按日期切分的数据湖,和一个按日期、地区、渠道切得很碎的数据湖,比较文件数和大小分布,并测量作用在分区键上的条件与没有作用在分区键上的条件,所打开的文件数有多大差别。然后执行文件合并,在合并过程中把它杀掉,确认通过清单读取的一方和遍历目录的一方分别看到了什么。最后更换键重新写入,并记下这一代价。评分器每次都会用不同的原始数据和目标大小真正运行你的工具,并核对答案。