1. 列式存储的起源:从行存储到列存储的演进

大家好,我是这次课程的主讲。今天咱们聊聊列式存储的起源。

说实话,我刚开始做数据库相关工作时,脑子里只有行存储。那时候觉得,数据嘛,一行一行存,天经地义。直到有一次,我接手了一个报表系统优化任务——几十亿行数据,跑一个简单的聚合查询要等十几分钟。老板站在我身后,盯着屏幕,那滋味……嗯,你们懂的。

从那以后,我开始认真研究:为什么行存储在大数据场景下这么慢?列存储又是怎么解决这个问题的?

1.1 行存储:传统时代的默认选择

先说说行存储。它的逻辑很直观:把一条记录的所有字段连续存放在磁盘上。

比如一张用户表:

用户ID | 姓名 | 年龄 | 城市 | 注册时间
----------------------------------------
1001   | 张三 | 28   | 北京 | 2023-01-15
1002   | 李四 | 35   | 上海 | 2023-02-20
1003   | 王五 | 22   | 广州 | 2023-03-10

在行存储中,磁盘上的布局是这样的:

[1001, 张三, 28, 北京, 2023-01-15] [1002, 李四, 35, 上海, 2023-02-20] [1003, 王五, 22, 广州, 2023-03-10]

这种模式在OLTP(在线事务处理)场景下非常合适。为什么?因为OLTP查询通常是「取出某一行的大部分字段」。比如:

SELECT * FROM users WHERE user_id = 1002;

行存储只需要一次I/O,就能把整行数据全部读出来。完美匹配。

行存储的核心优势:一次I/O获取完整记录,适合频繁的增删改操作和点查询。

1.2 行存储在大数据场景下的困境

但问题来了。到了大数据分析场景,查询模式完全变了。

你想想看,分析型查询长什么样?

SELECT 城市, COUNT(*) FROM users GROUP BY 城市;

这个查询只需要「城市」这一列。但在行存储里,它必须把每一行的所有字段都读进内存,然后再把不需要的字段扔掉。

我给大家算笔账:

场景 行存储读取量 实际需要量 浪费比例
查询1列(10列总宽) 100% 10% 90%
查询2列(20列总宽) 100% 10% 90%
聚合计算(1列) 100% 10% 90%

看到了吗?90%的I/O都是浪费的。 磁盘I/O是数据库最慢的环节,你却在拼命读那些根本用不上的数据。

我曾经优化过一个客户案例,他们的表有80多列,但分析查询只用其中3列。行存储下,每次查询要读80列的数据。换成列存储后,只读3列。你猜速度提升了多少?20倍以上。

注意:行存储的I/O浪费不是小问题。在大数据场景下,磁盘带宽是稀缺资源。读不需要的数据,就是在浪费查询时间。

1.3 列存储的诞生:换个思路存数据

那列存储是怎么做的呢?很简单——按列存,而不是按行存。

还是刚才那张用户表,列存储的磁盘布局是这样的:

用户ID列: [1001, 1002, 1003]
姓名列:   [张三, 李四, 王五]
年龄列:   [28, 35, 22]
城市列:   [北京, 上海, 广州]
注册时间列: [2023-01-15, 2023-02-20, 2023-03-10]

每一列的数据连续存放在一起。查询时,只需要读取涉及的列即可。

还是那个查询:

SELECT 城市, COUNT(*) FROM users GROUP BY 城市;

列存储只需要读取「城市」这一列的数据。其他4列,碰都不碰。

我的建议:如果你刚开始接触列存储,可以把它想象成「把数据库表竖着切了一刀」。每一列变成一个独立的存储单元。查询时按需加载,绝不浪费。

1.4 列存储的三大核心优势

除了减少I/O,列存储还有几个「杀手锏」特性。我一个个说。

1.4.1 更高的压缩率

同一列的数据类型相同,值往往有重复或规律。这就给压缩提供了巨大空间。

举个例子,「城市」列:

[北京, 上海, 北京, 广州, 上海, 北京, 深圳, 北京]

用字典编码压缩:

字典: 北京->0, 上海->1, 广州->2, 深圳->3
压缩后: [0, 1, 0, 2, 1, 0, 3, 0]

8个字符串,每个按10字节算,原来80字节。压缩后8个整数,每个2字节,才16字节。压缩率80%。

行存储呢?不同数据类型混在一起,压缩算法很难生效。我见过很多行存储的表,压缩率连20%都不到。

1.4.2 向量化计算

这个特性很多人容易忽略。列存储天然适合向量化处理。

什么意思?CPU一次可以处理一批数据,而不是一条一条处理。比如计算所有用户的年龄总和:

// 行存储:逐行处理
for each row:
    sum += row.age

// 列存储:向量化处理
ages = load_column("age")
sum = vector_sum(ages)  // CPU一次处理多条数据

向量化能充分利用CPU的SIMD指令集,处理速度能提升5-10倍。我在项目中测试过,同样的数据量,向量化比逐行处理快了将近8倍。

1.4.3 延迟物化

这个概念听起来有点绕,我简单解释一下。

列存储在执行查询时,不会急着把数据拼成完整的行。它会先在列上做过滤、聚合、计算,最后才把结果拼起来。

比如这个查询:

SELECT 姓名, 年龄 FROM users WHERE 城市 = '北京';

列存储的执行流程:

  1. 读取「城市」列,找到所有值为「北京」的行号(比如第1、3、8行)
  2. 只读取这些行对应的「姓名」和「年龄」列
  3. 最后才拼成结果行

这样做的好处是:中间过程处理的数据量极小。 如果「北京」只占10%,那90%的数据根本不会进入后续处理环节。

总结一下列存储的三大优势:

  • 按需读取:只读需要的列,I/O量减少80%-90%
  • 高压缩率:同类型数据集中存储,压缩比可达5:1甚至10:1
  • 向量化+延迟物化:计算效率大幅提升,内存占用更低

1.5 行存储 vs 列存储:一张图看懂

下面这张图展示了两种存储方式的核心差异:

行存储 vs 列存储:存储布局对比 行存储 行1: [ID, 姓名, 年龄, 城市, 注册时间] 行2: [ID, 姓名, 年龄, 城市, 注册时间] 行3: [ID, 姓名, 年龄, 城市, 注册时间] 行4: [ID, 姓名, 年龄, 城市, 注册时间] 查询年龄列 → 必须读取所有行所有列 列存储 ID列: [1001, 1002, 1003, 1004] 姓名列: [张三, 李四, 王五, 赵六] 年龄列: [28, 35, 22, 31] ← 只读这一列 城市列: [北京, 上海, 广州, 深圳] 注册时间列: [2023-01, 2023-02, 2023-03, 2023-04] 查询年龄列 → 只读取年龄列,其他列跳过 核心差异:行存储按行连续存放,列存储按列独立存放 分析查询只需读取少数列 → 列存储I/O量减少80%-90% 💡 我的经验 我曾经把一个行存储的报表系统迁移到列存储 同样的硬件,查询耗时从15分钟降到了30秒以内

1.6 为什么大数据时代需要列存储?

说到底,不是行存储不好,而是场景变了。

传统OLTP场景下,数据量不大(百万级),查询以点查为主。行存储完全够用。

但到了大数据时代:

  • 数据量爆炸:从GB级到TB甚至PB级
  • 查询模式变了:从「查一行」变成「分析千万行」
  • 响应要求高了:从「跑个报表等半小时」变成「秒级响应」
  • 成本压力大了:磁盘、内存、CPU都要精打细算

列存储正好命中这些痛点。我见过太多案例了——同样的查询,行存储跑几分钟,列存储几秒钟搞定。这不是魔法,是存储架构的降维打击。

一句话总结:行存储适合「查一行」,列存储适合「算万行」。大数据分析是后者的主场。

1.7 避坑指南:列存储不是银弹

说了这么多列存储的好处,我也得泼点冷水。

列存储不适合的场景:

  • 频繁的逐行插入/更新:列存储的写入性能通常不如行存储。因为要更新多个列文件,事务开销大。
  • 点查询(查单行全部字段):列存储需要读取多个列文件再拼装,反而比行存储慢。
  • 小数据量:数据量在百万级以下时,列存储的优势不明显,反而增加复杂度。

我曾经有个客户,非要把OLTP系统也换成列存储。结果呢?写入性能掉了80%,点查询也变慢了。最后老老实实换回了行存储。

我的建议:选型时先问自己三个问题——数据量多大?查询是分析型还是事务型?对写入性能要求高不高?想清楚再选。

1.8 本章小结

好了,咱们把这一章的核心内容捋一捋:

  • 行存储:按行连续存放,适合OLTP点查询
  • 列存储:按列独立存放,适合OLAP分析查询
  • 三大优势:按需读取、高压缩率、向量化+延迟物化
  • 适用场景:大数据分析、报表系统、数据仓库
  • 注意避坑:不适合频繁写入和点查询场景