列式存储原理
问题
什么是列式存储?为什么 OLAP 系统普遍采用列式存储?
答案
行式存储 vs 列式存储
| 维度 | 行式存储 | 列式存储 |
|---|---|---|
| 存储方式 | 按行存储 | 按列存储 |
| 读取粒度 | 整行(所有列) | 只读需要的列 |
| 压缩率 | 低(混合类型) | 高(同类型) |
| 聚合查询 | 慢(全行扫描) | 快(只扫描目标列) |
| 点查/更新 | 快(一次 IO 读整行) | 慢(需组装多列) |
| 典型系统 | MySQL、PostgreSQL | ClickHouse、Doris |
| 适合场景 | OLTP | OLAP |
IO 优势示例
假设表有 100 列、1 亿行:
SELECT SUM(amount), COUNT(*) FROM orders WHERE date = '2024-01-01';
| 存储方式 | 读取数据量 |
|---|---|
| 行式存储 | 100 列 × 1 亿行 = 全量数据 |
| 列式存储 | 2 列(amount + date)= 2% 数据 |
核心优势
列式存储只扫描查询涉及的列,在宽表场景下 IO 减少可达 50~100 倍。
压缩优势
同一列的数据类型相同,压缩效果远好于行式存储:
| 压缩算法 | 适用列类型 | 压缩率 |
|---|---|---|
| LZ4 | 通用,速度快 | 2~4x |
| ZSTD | 通用,压缩率更高 | 3~8x |
| Delta | 时间戳、递增 ID | 10~50x |
| Dictionary | 低基数字符串(如 city) | 5~20x |
| Run-Length | 排序后重复值多 | 10~100x |
向量化执行
向量化执行引擎一次处理一批数据(通常 1024~4096 行),充分利用 CPU Cache 和 SIMD 指令:
- CPU Cache 友好:连续内存访问,减少 Cache Miss
- SIMD 指令:单条指令并行处理多个数据
- 减少虚函数调用:批量处理减少函数调用开销
常见列式存储格式
| 格式 | 使用者 | 特点 |
|---|---|---|
| Parquet | Spark、Hive、Doris | 嵌套结构、行组内列式 |
| ORC | Hive、Presto | 轻量级索引、ACID 支持 |
| Arrow | 内存列式格式 | 跨语言零拷贝 |
| 自定义格式 | ClickHouse | 高度优化,深度集成 |
Parquet 文件结构
Parquet 按**行组(Row Group)+ 列块(Column Chunk)**组织:
- 行组:一组行的集合(通常 128MB)
- 列块:行组中某一列的数据
- 页(Page):列块内的最小读取单元
常见面试问题
Q1: 列式存储为什么不适合 OLTP?
答案:
OLTP 场景特点是高并发单行读写:
- 读取一行需要从多个列文件中分别读取再组装,IO 次数多
- 插入/更新一行需要写入多个列文件,写放大严重
- 行式存储一次 IO 即可读写完整行,更适合 OLTP
Q2: Parquet 和 ORC 怎么选?
答案:
| 维度 | Parquet | ORC |
|---|---|---|
| 生态 | Spark 原生 | Hive 原生 |
| 嵌套结构 | ✅ 原生支持 | ⚠️ 有限支持 |
| 压缩率 | 高 | 略高于 Parquet |
| 索引 | 统计信息 | 轻量级索引 |
| 推荐 | Spark/Doris 场景 | Hive 场景 |
Q3: 什么是编码(Encoding)和压缩(Compression)的区别?
答案:
- 编码:利用数据特征进行转换(如 Delta、Dictionary、RLE),通常在写入时即完成
- 压缩:对编码后的数据进一步压缩(如 LZ4、ZSTD),减小存储空间
- 两者可叠加使用,先编码再压缩,效果最佳
相关链接
- ClickHouse - 列式存储的典型实现
- Doris/StarRocks - MPP + 列式存储
- Hive - ORC/Parquet 文件格式的使用者