1. 列式存储概述:行式存储 vs 列式存储,核心概念与适用场景

大家好,我是老张。今天咱们开始聊列式存储。说实话,我最早接触列存是在做电信计费系统的时候——一张几百列的表,每次查询只取其中几列,但行存硬是把所有列都读了一遍,磁盘 I/O 高得吓人。那时候我就想,有没有一种存储方式,能只读我需要的列?后来才知道,这就是列式存储的核心思想。

1.1 行式存储:我们最熟悉的老朋友

行式存储,说白了就是把一行数据的所有列连续地存在一起。你想想看,MySQL、PostgreSQL 这些传统关系型数据库,底层基本都是行存。

行存的核心特征: 一个数据页里存的是完整的多行数据。读一行,所有列都拿到了。

举个例子,一张用户表有 100 列,但你的业务只需要查用户的姓名和手机号。行存的做法是:先把整行 100 列都读进内存,再从中挑出姓名和手机号。浪费吗?确实浪费。但为什么行存还这么流行?因为它适合 OLTP 场景——一次读写一整行,事务处理非常自然。

特性 行式存储 列式存储
数据组织 按行连续存储 按列独立存储
I/O 模式 读取整行 只读需要的列
压缩率 较低(列内数据类型混杂) 较高(同列数据类型一致)
典型场景 OLTP、点查、频繁更新 OLAP、聚合分析、大范围扫描
代表系统 MySQL、PostgreSQL、Oracle ClickHouse、Parquet、ORC

1.2 列式存储:换个角度看数据

列式存储的思路正好反过来——把同一列的所有值存在一起。比如一张表有 3 列:id、name、age。行存是 (id1,name1,age1),(id2,name2,age2)... 列存则是 [id1,id2,id3...], [name1,name2,name3...], [age1,age2,age3...]。

这样做有什么好处?我举一个实际例子。之前帮一家电商公司做用户行为分析,一张表有 200 列,每天新增 5 亿行。分析需求是「统计过去 7 天每个用户的登录次数」。如果用行存,每次查询要扫描 200 列 × 35 亿行 = 7000 亿个字段。换成列存,只需要读「用户ID」和「登录时间」两列,数据量直接降到 1/100。

我的经验: 列存最适合「宽表少列查询」的场景。表越宽,列数越多,列存的优势越明显。我曾经在 500 列的宽表上做过测试,列存比行存快了两个数量级。

1.3 核心概念:列族、列组与投影

聊列式存储,有几个概念必须搞清楚。

  • 列族(Column Family): 把经常一起查询的列分到一组。比如 HBase 里,用户基本信息放一个列族,行为日志放另一个列族。这样查询时只需要扫描相关的列族,不用碰其他数据。
  • 列组(Column Group): 比列族更细的粒度。Parquet 和 ORC 里支持把多个小列合并成一个列组存储,减少小文件数量。我建议:如果两列经常同时被查询,而且数据类型相似,可以考虑放到一个列组里。
  • 投影(Projection): 预计算并存储某些列的聚合结果。比如 Vertica 的 projection 机制,可以提前把「按天统计的销售额」存好,查询时直接读结果,不用再聚合。嗯,这里要注意:投影会占用额外存储空间,需要在查询速度和存储成本之间做权衡。

避坑指南: 我曾经在一个项目里把所有列都设成了独立列族,结果小文件数量爆炸,元数据管理成了噩梦。列族的粒度要适中,一般建议 5-10 个列一组比较合理。

1.4 列式存储的适用场景

不是所有场景都适合列存。我总结了几条判断标准:

  1. 分析型查询为主: 查询涉及大量行,但只取少数列。比如 BI 报表、用户画像、实时 OLAP。
  2. 数据批量写入: 列存对单行插入不友好,但批量导入效率极高。我见过 ClickHouse 每秒写入几百万行。
  3. 高压缩比需求: 同列数据类型一致,可以用 RLE、字典编码、差分编码等算法。我曾经把 100GB 的日志数据压缩到 8GB,压缩比超过 12:1。
  4. 不可变或追加写为主: 列存对更新和删除的支持较弱。如果业务需要频繁修改单行数据,行存更合适。

反过来,如果你的业务是「频繁插入单行、经常更新、查询总是取整行数据」,那行存还是更合适。没有银弹,选型要看场景。

1.5 列式存储的核心逻辑

下面这张图展示了列式存储的核心逻辑。你可以看到,数据从行式布局被拆解为按列独立存储,查询时只扫描需要的列,大幅减少 I/O。

列式存储核心逻辑 行式存储布局 Row1: [id1, name1, age1, city1, ...] Row2: [id2, name2, age2, city2, ...] Row3: [id3, name3, age3, city3, ...] ... 更多行 列式存储布局 id列: [id1, id2, id3, ...] name列: [name1, name2, name3, ...] age列: [age1, age2, age3, ...] city列: [city1, city2, city3, ...] 查询时:只扫描需要的列(如 name + age),跳过无关列 I/O 量 = 查询列数 / 总列数 × 行存 I/O,宽表场景优势巨大

1.6 列式存储的典型系统

目前主流的列式存储系统,我简单列几个:

  • ClickHouse: 纯列存,支持实时 OLAP。我最近两年用得最多,单表查询性能极强。
  • Apache Parquet: 列式文件格式,Hadoop/Spark 生态标配。适合离线数仓。
  • Apache ORC: Hive 的默认列存格式,压缩率比 Parquet 略高。
  • Vertica: 商业列存数据库,投影机制是亮点。
  • HBase: 宽表存储,按列族组织,适合稀疏表。

我的建议: 如果你刚开始接触列存,可以从 ClickHouse 入手。部署简单,文档齐全,而且你能直观感受到列存带来的性能提升。我第一个列存项目就是用 ClickHouse 做的,3 天就上线了。

1.7 小结

这一章我们聊了行存和列存的核心区别。说白了,行存适合「一次取一行」,列存适合「一次取一列」。选型时记住一句话:OLTP 用行存,OLAP 用列存。当然,实际场景往往更复杂,但这是最基础的判断标准。