2026-10-26·par 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 数、合并滞后)。报数,不报形容词。
à propos de cette entrée

L'une des entrées de wiki de sijie. L'IA sur ce site est fondée sur le même corpus et répond à la voix de sijie, avec citations revenant à des entrées comme celle-ci — répondre coûte argent à sijie, donc cela attend derrière un code : entrer un code d'accès →