Skip to content

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 列。

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 个列各自读出一个值再拼回一行。
  • 更新困难,三个原因层层递进:
    1. 数据分散:各列物理上不在一起;
    2. 编码依赖:Delta 这类编码改一个值,后面所有值都受影响;
    3. 压缩块:必须解压整块 → 改 → 重压 → 写回。
  • 常见应对(Parquet 干脆设计为不可变文件):Copy-On-Write 写新版本、Merge-On-Read 维护增量层读时合并、定期 Compaction 后台合并。

6 对比总结 ​

特征行式存储列式存储
场景OLTPOLAP
查询模式少量行、全部列大量行、少数列
写入模式频繁小批量低频大批量
代表产品MySQL、PostgreSQLParquet、ORC、ClickHouse
压缩比2~4 倍5~20 倍

7 那 Parquet 做了什么 ​

Parquet = 列存思想 + 四项工程化设计:

  • Row Group:水平切分,纯列存与数据局部性之间的折中;
  • 丰富编码:RLE、Dictionary、Delta、Bit-Packing 按列选择;
  • 嵌套数据支持:Repetition Level / Definition Level(源自 Dremel);
  • 丰富元数据:统计信息支撑谓词下推、列裁剪落到文件内部。

这些分别在后面三篇展开(文件结构、编码、Schema)。

面试追问链 ​

  • 列存为什么快?→ 列裁剪只读需要的列(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,引用请遵原文。