2026-10-26·by Sijie Wang#fundamentals#storage#columnar#olap

columnar-storage

列存储:每列存到一起,让扫描跳过其余一切

它是干嘛的。 分析查询读少数列、很多行(比如几百万行算 avg(price)),很少单行写。行存为用一列而读整行,还要维护查询根本用不上的逐行索引。列存是为扫描重新排字节。

它去掉的浪费(同一查询,行存那边)。

  • 索引白占:b-tree 只在选择性查询上帮忙。全表聚合的选择性约 100%,索引省不掉任何一次行访问,只剩存储与写维护成本。
  • 行本身白读:heap 页里一行 20 列全排着,只用 1 列的查询读进 19/20 废字节,还污染 buffer pool。

布局。 每列的值连续存在自己的文件里,各列行顺序一致。只读一列就只碰这一列的页:20 列的表读 1 列,约 1/20 的 IO,压缩之前。

寻址:下标,不是 指针。 行在各列里的位置就是它的地址:第 N 行的 price 就是 price 列第 N 个值。不存指针列。

  • 指针列要付三笔账:每行 8 字节;顺序扫描变指针追逐(一行一次 cache miss,打断向量化);地址熵高、压不动;每次合并都要重写。存一列顺序 id(0、1、2……)更是纯冗余:位置本身已经编码了它。
  • 指针是点查的答案(行存 B 树叶里存 TID 就是这个);下标是扫描的答案。

跳过:三档粗粒度元数据,同一个思想,粒度不同。

  • 分区:最粗、手动、一个键(比如按月 PARTITION BY)。查询带分区键时整目录剪掉。
  • Zone map:较细、自动、所有列。每块存每列 min/max,范围对不上的块整块跳过。
  • 稀疏主键:最细。每 granule(比如每 8192 行)记一个标记,不对每行建;定位到 granule 再顺序读。
  • 分区剪枝只对分区键有效;zone map 所有列都有,是兜底。分区过细会反噬(分区爆炸:小文件一堆,合并与元数据开销吃掉剪枝收益),所以按月这种低基数分,别按 user_id 分。

写:不可变 part 加后台合并(part 的一生)。

  1. INSERT 先在内存里按 ORDER BY 键排好序。
  2. 排好的数据写成一个新 part(每列一个文件,加 min/max、标记、校验和),提交前不可见。
  3. 提交是原子的:part 要么整个在,要么不在,没有写一半的 part。
  4. 查询开始时锁定当时的 part 集合;part 在查询过程中不变,所以读天然一致,不需要版本链。
  5. 后台合并挑同分区的小 part 归并成大 part,原子替换;读查询要么读到旧 part 组,要么读到新 part,永远不会读到一半新一半旧。
  6. UPDATE/DELETE 不是原地改:记一条 mutation,后台把受影响的 part 读出来、改完、写成新 part 再替换。重型、异步。
  7. TTL 到期整 part 或整分区删掉,几乎免费。
  8. 崩溃:临时目录里没提交的半成品 part 直接丢弃,已提交的 part 全在,不需要重放日志。

为什么不要 WAL 和 MVCC。 它俩都是为“原地改”服务的:wal 保写一半的行,mvcc 让读看到旧版本(代价见 isolation-levels)。列存的负载是批量追加写、极少改删、查询是扫,所以:

  • WAL 是纯开销:写放大一倍加检查点加回放,而 part 级原子提交已经给了崩溃安全。根本没有“写一半的行”需要日志去救。
  • MVCC 是纯负担:版本链会让扫描读到死版本、逐行判可见性。扫的活最怕这个;而不可变 part 加查询锁定 part 集合已经给了读一致性,MVCC 要解决的冲突根本不会出现。
  • 一句话:一旦决定“不原地改,只追加新 part、后台合并”,WAL 和 MVCC 要解决的问题就不存在了,剩下的只是 part 级原子提交加合并调度。

底下的假设。 这是对业务的押注,不是自然法则:统计类数据一旦落下来就不再动。日志、事件、指标、历史订单。发生过就是发生过,修正以追加新记录来,不以单行改写来。这个押注一破(天天单行改状态的业务,如用户资料、订单状态),列存就是错的工具;所以 HTAP 才把行存管交易、列存副本管分析拼在一起。同一招在行存那边叫 LSM:不可变文件加后台合并(本货架下一节)。

代价,老实讲。

  • 单行点查、单行改删贵(要重写整个 part)。
  • 跨 part 没有传统事务:INSERT 是 part 级原子,不是多 part 事务;重复写靠插入块去重兜着。
  • 小批量写太碎会产生一堆小 part,合并追不上就出经典的 "too many parts" 问题。列存喜欢批量写,不喜欢一行一行喂。

给 agent 的检查清单。读完本节点,按每一条去验系统,验出结果要带证据(文件、配置或查询计划)。

  1. 负载:写是不是追加为主、读是不是少列多行的扫描?如果单行改动占主导,判:工具选错,建议行存或 HTAP 分流。
  2. 布局:每列是不是连续存、各列行顺序是不是一致?引用文件布局作证。
  3. 寻址:下标还是存了指针?如果存了指针,算账:每行几字节、扫描行为、合并重写成本。
  4. 跳过元数据,三档都要:分区键加剪枝行为;zone map(每块每列 min/max?);稀疏索引粒度(每标记几行)。每档各举一个被剪掉的查询。
  5. part 一生:part 是不是不可变?提交是不是原子?合并是不是后台且不阻塞读?UPDATE/DELETE 与 TTL 各怎么实现?
  6. WAL/MVCC:是不是缺席?缺席是不是由不可变性正当化,还是其实存在原地写路径需要它们?
  7. 假设:追加为主这个假设有没有写下来?迟到数据、修正、删除各怎么走?
  8. 代价:点查成本、单行改动成本、小批量行为(part 数、合并滞后)。报数,不报形容词。
about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

columnar-storage — Sijie Wang