存储布局:页、行存与列存

kp-019 · 存储引擎与日志核心约 25 分钟已校对

前置知识点

前置:

相关知识点

相关:

学习进度:

一句话定义

存储布局决定「一行数据在磁盘上如何摆放」:行存(Row Store)把整行连续放在页内,利于取整行;列存(Column Store)把同列值连续存放,利于按列扫描与压缩——OLTP 与 OLAP 引擎的分野由此而来。

为什么重要

前置知识

kp-001(内模式)、kp-009(页与索引的关系)。

核心概念

原理与机制

行存的代价模型:取整行 = 1 次页读;但「求所有订单的金额之和」要把每页整行读入再丢弃 90% 的列,IO 与 CPU 双重浪费。列存把「金额列」单独连续存放:扫描只读目标列的字节,天然向量化(SIMD 一批处理同类型值),且同列值相似度高、字典/RLE 编码后压缩比常达 5~10 倍,实际 IO 更小。取整行则相反:列存要按行号从 N 个列段各取一段再拼装,OLTP 点查不友好。这就是布局-负载的匹配律:读多写少、宽表聚合 → 列存;高频点查、整行事务 → 行存。写入侧行存支持原地更新(配合 Undo),列存多为追加 + 后台合并(与 kp-021 的 LSM 思想同源)。

实例或案例

同一张 10 亿行订单表:MySQL(行存 InnoDB)SELECT SUM(amount) FROM orders 需扫全部数据页(数十 GB 级 IO、分钟级);ClickHouse(列存 + 编码)只读 amount 列(压缩后可能仅数 GB,向量化扫描)毫秒到秒级返回。反向:SELECT * FROM orders WHERE order_id = ? 在 ClickHouse 需访问所有列段拼行,而 MySQL 一次 B+ 树点查即可。两类引擎在各自战场快 1~2 个数量级、在对方战场慢一个量级以上——选型依据是负载形状,不是品牌。

常见误区

自测题

  1. 为什么列存对 SELECT COUNT(*) 几乎免费? 答:任取一个最小列段扫行号/标记即可计数,配合元数据(行数)甚至可常数时间回答。
  2. HTAP 产品的常见做法是什么? 答:同一系统内维护行存副本(服务 TP)与列存副本(服务 AP),由同步机制保持近实时一致,如 TiFlash、Oracle In-Memory。

公式或模型

扫描 IO 估算:行存读整表 ≈ N × row_width 字节;列存读 k 列 ≈ N × Σ col_width_i × compression_ratio_i(k ≪ 总列数时优势呈比例放大)。压缩比经验区间:低基数列 RLE 5~20 倍,高基数列接近 1。

图示

行存页:[行1][行2][行3]… 读一行 = 一次页读 聚合要读入全部列(浪费) id 列段 amount 段 city 列段 ts 列段 列存:聚合只扫需要的列段
行存按行聚集、列存按列聚集:负载形状决定哪种布局占优

直观类比

行存像档案柜按「人」抽屉存放:调一个员工的全套资料一次搞定;列存像所有员工的工资条夹一本、合同夹一本——统计「全员工资总和」只翻一本册子,但要齐一个人的全套材料就得跑遍每个柜子。

与其他知识点的关系

kp-022 的缓冲池按页运转;kp-021 的 LSM 是写入侧的另一种组织;kp-030 的选型框架把负载形状作为第一判据。

延伸阅读