2. 核心概念:行式存储 vs 列式存储,数据在磁盘上的物理布局差异

聊列式存储之前,咱们得先搞清楚一个最根本的问题:数据在磁盘上到底是怎么躺着的?

你想想看,同样的数据,用不同的姿势摆放在磁盘上,查询性能的差距能差出几个数量级。这不是玄学,这是物理规律决定的。

我早年刚接触数据库内核时,总觉得存储格式就是个序列化问题,把数据写进去能读出来不就完了?直到有一次在生产环境调优一个报表查询,同样的硬件,换个存储格式查询时间从 47 秒降到了 0.8 秒。嗯,从那以后我再也不敢轻视物理布局了。

2.1 行式存储:一条记录就是一个整体

行式存储,说白了就是把同一行的所有列挨个儿串在一起,然后一行接一行地往下排。

拿一张用户表举例:

id name age city
1 张三 28 北京
2 李四 35 上海
3 王五 22 广州

在行式存储的磁盘上,物理布局大概是这样的:

磁盘块 1: [1|张三|28|北京][2|李四|35|上海]
磁盘块 2: [3|王五|22|广州]...

每一行内部,列与列之间紧挨着。读一行数据,一次 I/O 就能把整行全部捞出来。这有什么好处?

  • 点查快:按主键查某条记录,一次磁盘读取搞定
  • 写入友好:插入一条新记录,就是追加一个连续块
  • 事务简单:整行数据在物理上连续,回滚和锁管理都方便

但问题来了——如果你只想查所有人的年龄呢?

行式存储会把整行数据都读到内存里,哪怕你只需要一个 age 字段。id、name、city 这些你根本用不上的数据,也被迫跟着一起加载。这就是典型的 I/O 放大。

核心痛点:行式存储的 I/O 放大倍数 = 行宽 / 目标列宽。如果一张表有 50 个字段,你只查其中 1 个,I/O 放大就是 50 倍。

2.2 列式存储:按列分家,各过各的

列式存储的思路完全不同——把同一列的所有值放在一起。

还是上面那张用户表,列式存储的物理布局是这样的:

磁盘块 1: [1][2][3]...          ← id 列
磁盘块 2: [张三][李四][王五]...   ← name 列
磁盘块 3: [28][35][22]...        ← age 列
磁盘块 4: [北京][上海][广州]...   ← city 列

每列单独存储,列与列之间在磁盘上不连续。这意味着什么?

  • 按需读取:查 age 列,只加载 age 对应的磁盘块
  • 压缩率高:同一列的数据类型相同,值重复度高,压缩算法能发挥最大威力
  • 向量化友好:连续的内存布局适合 SIMD 指令批量处理

我参与过一个广告数据分析平台的项目,原始数据 200 多列,业务方每天跑一次聚合查询,要扫描全表。用行式存储时,每次查询要扫 2TB 数据;改成列式存储后,只扫聚合用到的 5 列,数据量降到 50GB,加上列式压缩,实际 I/O 只有 8GB。查询时间从 20 分钟降到 40 秒。

一个小技巧:列式存储的压缩效果跟列的数据分布强相关。低基数列(如性别、状态码)压缩比能到 20:1 以上;高基数列(如 UUID、时间戳)压缩比可能只有 2:1。设计表结构时,尽量把低基数字段放在一起,能省不少存储成本。

2.3 两种布局的直观对比

我画了一张图,帮你把两种布局的差异刻在脑子里:

行式存储 vs 列式存储 · 磁盘物理布局对比 行式存储 行1: [1 | 张三 | 28 | 北京] 行2: [2 | 李四 | 35 | 上海] 行3: [3 | 王五 | 22 | 广州] 行4: [4 | 赵六 | 30 | 深圳] 查询 age 列 → 必须读取全部 4 行(I/O 放大 4 倍) 列式存储 id列: [1][2][3][4] name列: [张三][李四][王五][赵六] age列: [28][35][22][30] city列: [北京][上海][广州][深圳] 查询 age 列 → 只读 age 列(I/O 缩小 4 倍) 关键差异 行式:一行数据物理连续,适合 OLTP 点查 列式:一列数据物理连续,适合 OLAP 分析 I/O 放大 = 行宽 / 目标列宽 I/O 缩小 = 只读目标列 全量 I/O 按需 I/O

2.4 适用场景:谁该用哪种?

没有银弹。两种存储格式各有各的舞台。我根据自己的经验,整理了一个对比表:

对比维度 行式存储 列式存储
典型代表 MySQL InnoDB, PostgreSQL ClickHouse, Parquet, ORC
查询模式 点查、小范围扫描 大范围聚合、分析
I/O 特征 读整行,I/O 放大 读目标列,I/O 缩小
压缩效果 差(混合类型,重复度低) 好(同类型,重复度高)
写入性能 优秀(追加写) 较差(需要重写整列)
事务支持 成熟(MVCC 天然适配) 较弱(列级事务复杂)
适合场景 OLTP(订单、用户、交易) OLAP(报表、BI、日志分析)

我曾经踩过的坑:有一回我把一个 OLTP 系统的订单表直接迁移到列式存储上,想着分析查询能快一点。结果业务方反馈写入延迟从 2ms 飙升到 200ms,因为每次插入一条订单,列式存储要更新 30 多个列文件。后来我加了一层行式缓冲区做写入合并,才把延迟降回来。记住:列式存储不适合高频单行写入。

2.5 混合存储:鱼和熊掌我都要

看到这里你可能会问:能不能两种都要?

能。现代数据库越来越倾向于混合存储。比如:

  • MySQL HeatWave:InnoDB 行式引擎 + HeatWave 列式引擎,自动路由查询
  • PostgreSQL + Citus:行式存储为主,列式扩展做分析
  • Delta Lake / Apache Iceberg:底层用 Parquet 列式存储,上层提供行级 ACID

我个人的习惯是:OLTP 用行式,OLAP 用列式,中间层用 CDC 同步。这样既能保证业务系统的写入性能,又能让分析查询跑得飞快。

一句话总结:行式存储为「找一条记录」而生,列式存储为「算一堆数据」而生。选哪个,取决于你的查询到底在「找」还是在「算」。