一句话定义
存储布局决定「一行数据在磁盘上如何摆放」:行存(Row Store)把整行连续放在页内,利于取整行;列存(Column Store)把同列值连续存放,利于按列扫描与压缩——OLTP 与 OLAP 引擎的分野由此而来。
为什么重要
- 「为什么分析查询要用 ClickHouse 而不是 MySQL」的答案就在布局:同样的 SQL,布局不同性能差百倍。
- 页是理解一切机制的最小单位:kp-009 的树节点、kp-020 的日志、kp-022 的缓冲池全部围绕页运转。
- HTAP(混合负载)产品(TiDB 行列混存、Oracle In-Memory)本质是把两种布局塞进一个系统。
前置知识
kp-001(内模式)、kp-009(页与索引的关系)。
核心概念
- 页(Page/Block):磁盘最小 IO 单位,InnoDB 16KB、PostgreSQL 8KB;页内含页头、行目录、数据区。
- 行存(NSM,n-ary storage model):页内行连续,读写整行一次定位。
- 列存(DSM,decomposition storage model):每列独立成段,值连续,配套编码压缩。
- 列编码:字典编码(RLE 游程压缩)、位打包(Bit-Packing)、Delta 编码。
- 元组标识(Tuple ID):列存中行还原靠行号对齐或 Id 列表。
- Heap 文件与段:表数据的物理容器,追加或按空闲页复用。
原理与机制
行存的代价模型:取整行 = 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 个数量级、在对方战场慢一个量级以上——选型依据是负载形状,不是品牌。
常见误区
- 用 OLTP 库跑大分析查询并归咎于「没优化 SQL」:布局决定上限,改写 SQL 救不了行存。
- 认为列存一定更省空间:编码收益依赖列基数与分布,高基数自由文本列压缩比可能很差。
- 把「分库分表」当成应对分析负载的方案:分表只拆分了扫描范围,没有改变每行的存储形态,治标不治本。
自测题
- 为什么列存对
SELECT COUNT(*)几乎免费? 答:任取一个最小列段扫行号/标记即可计数,配合元数据(行数)甚至可常数时间回答。 - HTAP 产品的常见做法是什么? 答:同一系统内维护行存副本(服务 TP)与列存副本(服务 AP),由同步机制保持近实时一致,如 TiFlash、Oracle In-Memory。
公式或模型
扫描 IO 估算:行存读整表 ≈ N × row_width 字节;列存读 k 列 ≈ N × Σ col_width_i × compression_ratio_i(k ≪ 总列数时优势呈比例放大)。压缩比经验区间:低基数列 RLE 5~20 倍,高基数列接近 1。
图示
直观类比
行存像档案柜按「人」抽屉存放:调一个员工的全套资料一次搞定;列存像所有员工的工资条夹一本、合同夹一本——统计「全员工资总和」只翻一本册子,但要齐一个人的全套材料就得跑遍每个柜子。
与其他知识点的关系
kp-022 的缓冲池按页运转;kp-021 的 LSM 是写入侧的另一种组织;kp-030 的选型框架把负载形状作为第一判据。
延伸阅读
- 《Architecture of a Database System》(Hellerstein、Stonebraker、Hamilton,2007)
- 《数据密集型应用系统设计》(Martin Kleppmann)第 3 章