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,你待会儿会亲手跑一遍。
同一份 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 这一列 → 只碰那一段字节,其余根本不从磁盘取 同类值挨在一起 → 字典/差分/游程编码全部生效
这个布局带来两个后果,一个省空间(上面已经看到),一个省时间:
count(*) 快到 0.2 ms 是因为它一行数据都没读——行数写在文件尾部的元数据里。第 2 课会拆开这个尾部。
那它不能干什么
这是本课最该记住的一段。Parquet 的每一条优点,背面都是一条限制:
| 它做不到 | 为什么 | 你该用什么 |
|---|---|---|
| 改一行 | 文件不可变。列被编码压缩成块,改一个值等于重写整个 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 写好,而不是急着上表格式。
把本课的数字亲手跑出来。只需要两个依赖:
$ 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 课会讲透,但你不用等到那时候才问。