Appearance
Parquet 深入理解(一):理解列式存储
系列第一篇。一切从一个问题开始:同样一份数据,换一种组织方式,查询速度为什么能差 10 倍以上。答案不在任何具体系统,而在行存与列存这对根本分歧。后端工程师天天用 MySQL(行存),又天天听说大数据全是列存——把这条分界线画清楚,数据选型题就再也难不倒。系列导航见 Parquet深入理解-总起,下一篇是 Parquet二-文件结构剖析。
1 问题设定:一张表、两种查询
- 表 users,1 亿行、6 列:id BIGINT、name VARCHAR(100)、age INT、city VARCHAR(50)、salary DECIMAL(10,2)、create_time TIMESTAMP。每行约 200 字节。
- 两种典型负载:
- OLTP:
SELECT * FROM users WHERE id = 12345——按主键取一行完整数据; - OLAP:
SELECT city, AVG(salary) FROM users GROUP BY city——扫全表但只要 2 列。
- OLTP:
2 行式存储:为 OLTP 而生
- MySQL/PostgreSQL 的方式:整行连续存放。
行1: | id=1 | 张三 | 28 | 北京 | 15000 | ts |
行2: | id=2 | 李四 | 35 | 上海 | 25000 | ts |
行3: | id=3 | 王五 | 42 | 北京 | 30000 | ts |- 点查完美:索引定位 → 一次 IO 读出整行 → 返回(B+ 树如何定位见 MySQL索引详解)。
- 分析场景致命伤是读放大(Read Amplification):只要 city 和 salary 两列,却必须把 6 列全部读进来。1 亿行 × 200B ≈ 20GB 磁盘 IO,其中有用数据只有约 2GB——90% 的 IO 是白读的。
3 列式存储:换个思路
- 同一列的值连续存放:city 列物理上就是
[北京, 上海, 北京, ...],salary 列[15000, 25000, 30000, ...]。 - 核心武器是列裁剪(Column Pruning):只读查询涉及的列——SELECT、WHERE、GROUP BY、ORDER BY 里出现的列。上面的查询 20GB IO 直接降到约 2GB。
- 带过滤也一样:
WHERE age > 30 AND create_time > '2023-01-01'需要读 4 列,仍好于 6 列全读。 - 真实数仓的表常有几十上百列,单次查询通常只碰 5~10 列——列越多、查询越窄,列存优势越大。
4 压缩优势:同列数据"长得像"
- 列内数据的两个特点:类型相同、值高度相似(大量重复),天然利好压缩。
- 字典编码:城市名建字典
{0:北京, 1:上海, 2:广州, 3:深圳},每个值从 6~12 字节压到 2~3 个 bit。 - 游程编码(RLE):排序后
[北京×4, 上海×3, 广州×2],9 个值变成 3 对(值, 次数)。 - 两个经验数字:排序后再存压缩效果更佳;编码常组合使用(Dictionary → RLE → Snappy 通用压缩)。压缩比量级:行存一般 2~4 倍,列存 5~20 倍。
5 列存的代价:没有免费午餐
- 写入成本高:行存追加一行 1 次 IO;列存要把一行拆到 N 个列文件,N 次 IO——所以列存系统都走批量写入。
- 单点查询效率低:按 id 取整行,得从 N 个列各自读出一个值再拼回一行。
- 更新困难,三个原因层层递进:
- 数据分散:各列物理上不在一起;
- 编码依赖:Delta 这类编码改一个值,后面所有值都受影响;
- 压缩块:必须解压整块 → 改 → 重压 → 写回。
- 常见应对(Parquet 干脆设计为不可变文件):Copy-On-Write 写新版本、Merge-On-Read 维护增量层读时合并、定期 Compaction 后台合并。
6 对比总结
| 特征 | 行式存储 | 列式存储 |
|---|---|---|
| 场景 | OLTP | OLAP |
| 查询模式 | 少量行、全部列 | 大量行、少数列 |
| 写入模式 | 频繁小批量 | 低频大批量 |
| 代表产品 | MySQL、PostgreSQL | Parquet、ORC、ClickHouse |
| 压缩比 | 2~4 倍 | 5~20 倍 |
7 那 Parquet 做了什么
Parquet = 列存思想 + 四项工程化设计:
- Row Group:水平切分,纯列存与数据局部性之间的折中;
- 丰富编码:RLE、Dictionary、Delta、Bit-Packing 按列选择;
- 嵌套数据支持:Repetition Level / Definition Level(源自 Dremel);
- 丰富元数据:统计信息支撑谓词下推、列裁剪落到文件内部。
面试追问链
- 列存为什么快?→ 列裁剪只读需要的列(20GB→2GB)+ 同列压缩比高;什么场景反而不快?→
SELECT *点查整行、高频小写入、频繁更新; - 行存为什么适合 OLTP?→ 整行连续存放,一次 IO 拿全行;读放大是什么?→ 只需部分列却读全部列的 IO 浪费;
- 列存更新为什么难?→ 数据分散 + 编码依赖 + 压缩块三连;怎么绕?→ COW/MOR/Compaction,Parquet 直接选择不可变;
- 为什么列存压缩比高?→ 同列类型相同且值相似,字典+RLE 直接吃掉重复;排序为什么影响压缩?→ 排序让重复值物理相邻,RLE 才有游程可吃。
面试官视角小结
行存 vs 列存是数据系统选型的第一道分水岭。能背"OLTP 行存、OLAP 列存"只是及格;能用 1 亿行 × 200B 算出 20GB vs 2GB 这笔 IO 账、说清列存三大代价及 COW/MOR 对策,才是理解了 trade-off 而非结论。最好的信号是候选人能反推:什么样的业务表放 Parquet 是错的——答得出这个,选型能力就立住了。
来源:javadoop《深入理解 Parquet(一):理解列式存储》(https://javadoop.com/post/parquet-01-understanding-columnar-storage)——本文为研读消化后的重写笔记,原文版权归 javadoop,引用请遵原文。