44.16 OLAP 与列式存储
一句话
行存按 “整行” 存,适合 “取一行所有字段”(点查/事务);列存按 “整列” 存,适合 “扫一列做聚合”(分析)——因为分析查询只碰少数几列,列存读的字节数天然少一个数量级,再加上同列同类型好压缩。
行存 vs 列存
| 维度 | 行存(OLTP) | 列存(OLAP) |
|---|---|---|
| 存储布局 | 一行连续 | 一列连续 |
| 点查(取整行) | 快(一次 IO) | 慢(要拼多列) |
| 聚合(扫一列) | 慢(读整行) | 快(只读该列) |
| 压缩 | 差(行内类型混杂) | 好(同列同类型) |
| 更新 | 就地更新 | 批量追加/合并 |
| 典型 | MySQL、PostgreSQL | ClickHouse、Doris、Parquet |
实测:只查 2 列时,行存与列存读多少字节
100 万行 × 6 列(id/uid/amt/city/ts/flag),查询 “按 city 求和 amt”:
| 布局 | 需要读取 | 说明 |
|---|---|---|
| 行存 | 82.2MB | 必须读整行(6 列全读) |
| 列存 | 9.5MB | 只读 city + amt 两列 |
| 减少 | 88% | 这就是列存 “扫描快” 的根源 |
再加上列内压缩:amt 列压缩后 33KB、city 列 25KB(同列同类型,压缩率远高于行存)。所以同样的查询,列存可能比行存少读 1–2 个数量级的字节。
-- 列存引擎会自动做 “列裁剪” + “谓词下推”
SELECT city, sum(amt) FROM orders WHERE ts >= '2026-01-01' GROUP BY city;
-- 只读 city/amt/ts 三列;ts 的 min/max 索引先跳过大半数据块
三个关键机制
| 机制 | 作用 | 收益 |
|---|---|---|
| 列裁剪 | 只读查询涉及的列 | 读字节 ∝ 涉及列数/总列数 |
| 谓词下推 + min/max 索引 | 跳过不满足条件的块 | 常能跳过 90%+ 数据块 |
| 向量化执行 | 按批(1024/4096 行)处理 | 减少函数调用、利于 SIMD |
反模式
| 反模式 | 后果 |
|---|---|
| 用列存做高并发点查 | 每次要拼多列,延迟高 |
| 用行存跑大范围聚合 | 读整行,IO 放大数倍 |
| 列存频繁单行更新 | 每次都是 “标记 + 后台合并”,写放大高 |
| SELECT * 查宽表 | 列裁剪失效,等于行存 |
| 不做分区/排序键设计 | min/max 索引跳过率低 |
| 小批量频繁导入 | 产生大量小文件,合并压力大 |
三条铁律
🔑 列存赢在 “只读需要的列 + 同列好压缩”,不在 “存储格式新”。
🔑 点查用行存,聚合用列存;混用两边都慢。
🔑 列存要设计排序键/分区,否则 min/max 索引形同虚设。
自检
Q1 为什么列存 “只读 2 列” 就能比行存少读 88%?
▸ 答案:因为行存把一行 6 列连续放在一起,读任何一列都要把整行读进来(82.2MB);列存把每列单独连续存放,只读 city 与 amt 两列(9.5MB)。读字节数 ≈ 涉及列数 / 总列数,列越多、查询涉及的列越少,差距越大。
Q2 列存为什么压缩率更高?
▸ 答案:因为同一列的数据类型相同、取值相近、有序性好:整数列可以用 Delta/位打包,低基数列可以用字典/RLE。行存里一行混着字符串、整数、时间戳,压缩算法很难找到跨行重复模式。
Q3 列存怎么做 “更新一行”?
▸ 答案:通常不就地改:要么写 “删除标记 + 新版本”(MergeTree 的 ALTER UPDATE 是异步 mutation),要么进 delta 存储后台合并。所以列存适合 “批量追加 + 少量更新”,高频单行更新会很贵。
Q4 什么是 “向量化执行”,为什么列存需要它?
▸ 答案:向量化是按批处理(一次 1024/4096 个值)而不是逐行处理,减少函数调用与分支,利于 CPU 流水线与 SIMD。列存的数据本来就是 “连续同类型”,天然适合按批处理;行存逐行处理则难以向量化。
Q5 为什么 “SELECT *” 在列存里是灾难?
▸ 答案:因为列裁剪失效:本来只读 2 列,SELECT * 要读全部列,读字节数回到行存水平甚至更差(列存每列单独 IO)。所以列存场景要显式列出字段,禁止 SELECT *。
Q6 列存的 “小文件问题” 从哪来?
▸ 答案:来自频繁小批量写入:每次写入生成一个新文件/分区,后台要不断合并。文件太多会导致元数据膨胀、查询要打开大量文件。解法是批量导入(攒够再写)+ 后台 compaction + 合理分区粒度。
Q7 什么时候该上列存而不是 “行存 + 索引”?
▸ 答案:当查询是 “大范围扫描 + 少数列聚合”(报表、BI、日志分析),且数据量大到 “行存读整行” 成为瓶颈时。反之,点查、事务、频繁更新仍是行存的强项;很多系统是 “行存做交易 + 列存做分析” 的组合。
关联
- 前置:44.1 存储引擎:B+ 树与 LSM 树、44.4 存储引擎:压缩与编码
- 并列:44.5 存储引擎:读写放大与空间放大、46.2 数据角色 (主数据 / 副本 / 派生数据)
- 下游:32.2 物化视图、44.21 数据库选型方法