课程首页· 术语表· 速查表· 第 1 课 / 共 6 课

Lesson 01 · 动机

307 MB 变 21.79 MB:为什么是列存

先建立"这东西凭什么值得学"的判断,再谈它的边界在哪。

Pivot 的楼宇 IoT 数据有一个很清晰的形状:写一次、几乎不改、按时间窗大批量读。一栋楼 200 个采点、每分钟一条,一年就是 200 × 525600 ≈ 1.05 亿行。这些行躺在 PostgreSQL 里,索引膨胀、vacuum 变慢、磁盘账单上涨,而 90% 的历史数据一个月都没人查一次——查的时候还偏偏是"给我 3 号楼上个季度的能耗曲线"这种要扫几千万行、只碰两三列的分析型查询。

这一课不教任何 API。目标只有一个:让你亲眼看到列存在这类数据上的量级差异,并且知道它不能干什么。

一个数据集,四种存法

下面所有数字都来自本课程的样本数据集:200 台设备 × 15 天 × 每分钟一点 = 4,320,000 行,7 列(时间、租户、楼栋、设备、指标名、数值、质量位)。生成脚本是 gen_sample.py,你待会儿会亲手跑一遍。

本机实测 · pyarrow 25.0.0 / macOS ARM
624.76JSON Lines (MB)
307.52CSV (MB)
266.49Parquet 关掉编码压缩 (MB)
31.73Parquet 默认 (MB)
21.79Parquet 排序后 (MB)

同一份 4,320,000 行数据。最右一列是 CSV 的 1/14、JSONL 的 1/29。

注意第三个数字 266.49 MB:这是把 Parquet 的编码和压缩全部关掉写出来的。它和 CSV 差不多大。这说明一件很重要的事——

Parquet 省空间,靠的不是"列存"这个布局本身,而是列存让同类数据挨在一起之后,编码和压缩才能发挥作用。

一列 device_id 里全是 AHU-05-013 这种高度重复的字符串,字典编码可以把它压成几个字节的索引;一列 ts 全是等间隔递增的整数,差分编码能把它压到几乎为零。而在行存里,这些值被 value、quality 这些完全不同类型的数据隔开,任何编码都无从下手。第 3 课会把这两层拆开细讲。

行存 vs 列存:磁盘上的区别

同样 3 行 4 列的数据,在磁盘上的字节顺序不同:

行存(PostgreSQL 的堆表、CSV)
┌─────────────────────────────────────────────────────┐
│ ts₁ dev₁ val₁ q₁ │ ts₂ dev₂ val₂ q₂ │ ts₃ dev₃ val₃ q₃ │
└─────────────────────────────────────────────────────┘
  想读 val 这一列 → 必须把整行都读进来,再丢掉 3/4

列存(Parquet)
┌──────────────┬────────────────────┬──────────────┬────────┐
│ ts₁ ts₂ ts₃  │ dev₁  dev₂  dev₃   │ val₁val₂val₃ │ q₁q₂q₃ │
└──────────────┴────────────────────┴──────────────┴────────┘
  想读 val 这一列 → 只碰那一段字节,其余根本不从磁盘取
  同类值挨在一起 → 字典/差分/游程编码全部生效

这个布局带来两个后果,一个省空间(上面已经看到),一个省时间:

本机实测 · DuckDB 1.5.5 · 取 5 次最好成绩
216.4 msCSV 上做一小时时间窗聚合
15.0 ms同一查询在 Parquet 上
185.6 msCSV 数行数 count(*)
0.2 msParquet 数行数 count(*)

count(*) 快到 0.2 ms 是因为它一行数据都没读——行数写在文件尾部的元数据里。第 2 课会拆开这个尾部。

那它不能干什么

这是本课最该记住的一段。Parquet 的每一条优点,背面都是一条限制:

Parquet 的边界(每一条都会在 Pivot 场景里遇到)
它做不到为什么你该用什么
改一行 文件不可变。列被编码压缩成块,改一个值等于重写整个 column chunk 甚至整个文件 热数据留在 PostgreSQL;冷层要改就整分区重写
高频单行写入 元数据在文件尾部,一次写入 = 一个完整文件。每秒写一条就是每秒一个小文件(第 5 课的小文件灾难) 先攒批(按小时/按天),再一次落盘
主键点查 没有 B-tree 索引。所谓"索引"只是每个数据块的 min/max 统计,命不中就得全扫 点查走 PostgreSQL;或靠排序键 + 分区把扫描面缩小(第 4 课)
事务 / 并发写 没有事务语义。两个进程同时写同一目录,谁也不知道对方写了什么 需要 ACID 就上表格式(Iceberg / Delta),第 6 课交代何时需要
做在线服务的存储层 为吞吐设计,不为延迟。单条查询要读元数据 + 定位 + 解压 它是分析层与归档层,不是 OLTP 存储

它在生态里的位置

补齐数据工程知识栈这件事,可以从一句话开始:Parquet 是地基,不是房子。

   查询引擎    DuckDB / Spark / Trino / ClickHouse / pandas / Polars
                              ↑ 都能直接读
   表格式      Iceberg / Delta Lake / Hudi     ← 加事务、时间旅行、schema 演进管理
                              ↑ 底下存的还是
   文件格式    ★ Parquet ★   ORC / Avro
                              ↑ 放在
   存储        S3 / OSS / MinIO / 本地磁盘 / HDFS

换句话说:你学 Iceberg 之前必须懂 Parquet,因为 Iceberg 管理的就是一堆 Parquet 文件 + 一份元数据清单。而绝大多数团队先需要的是把 Parquet 写好,而不是急着上表格式。

动手做 · 约 3 分钟(脚本跑 1 分钟)

把本课的数字亲手跑出来。只需要两个依赖:

$ mkdir parquet-lab && cd parquet-lab
$ python -m venv .venv && source .venv/bin/activate
$ pip install pyarrow duckdb
# 下载课程脚本(或从课程目录复制)
$ curl -O https://teach.sigia.com.cn/parquet/assets/gen_sample.py
$ python gen_sample.py

预期结果(约 1 分钟后):

生成 4,320,000 行 / 7 列
iot_naked.parquet             266.49 MB
iot_sorted.parquet             21.79 MB
iot_unsorted.parquet           31.73 MB
raw.csv                       307.52 MB
raw.jsonl                     624.76 MB

脚本里 random.seed(42) 是固定的,所以你的数字应该和这里一模一样。对不上就说明 pyarrow 版本不同(本课基线 25.0.0),先记下来,第 3 课会讲版本会影响什么。

这一课的小收获

列存省空间的真正原因是同类数据挨在一起后编码才生效(关掉编码压缩的 Parquet 和 CSV 一样大)。它省时间的原因是查询只碰它要的列,而 count(*) 甚至一行都不用读。代价是:不可变、无索引、无事务、不适合高频单行写——所以它是 Pivot 的冷层与分析层,不是 PostgreSQL 的替代品。

自测一下

把 Parquet 的编码和压缩全部关掉后,文件从 21.79 MB 涨到 266.49 MB,和 CSV 相当。这说明什么?

随时问我:如果你手上的任务是"把某张 PG 表导出归档",现在就可以把表结构发给我,我帮你判断它适不适合落 Parquet、该按什么排序、分区切多细——这些决定在第 4、5 课会讲透,但你不用等到那时候才问。

延伸阅读(本课推荐先读这一篇) Apache Parquet 官方 Overview——五分钟读完,官方对"Parquet 是什么、为谁设计"的自述,是本课程唯一的一手定义来源 · Concepts(术语定义) · 本课程术语表