列式存储(Column Store)在分析型查询(OLAP)中的优势早已被业界广泛认可。通过将同一列的数据连续存储在一起,列存引擎可以只读取查询涉及的列、获得极高的压缩比、充分利用 SIMD 向量化执行——这些特性使得列存在聚合、扫描、过滤等分析场景中拥有碾压行存的性能优势。
然而,这种为”读”而生的数据组织方式,天然地与”写”形成了矛盾。
在行存数据库中,插入一行数据只需要追加一条完整的记录。但在列存中,插入一行意味着要向每一列各追加一个值,而这些列往往分布在不同的文件、不同的压缩块中。更糟糕的是,列存通常对数据进行了深度压缩(字典编码、游程编码、位图编码等),任何就地修改都可能需要重写整个压缩块。随机的小批量写入在这种架构下的代价高得惊人。
Delta Store 正是为了解决这一根本性矛盾而诞生的缓冲机制。它的核心思想简单而优雅:用一个对写入友好的小型存储区来吸收实时的增量变更,再通过后台异步过程将这些变更合并到读优化的主存储中。
本文将从原理出发,深入剖析业界主流列式数据库中 Delta Store 的设计方案,包括学术界的奠基之作 C-Store/Vertica、内存列存巨头 SAP HANA、面向快数据的 Apache Kudu、为实时分析而生的 ClickHouse、以及微软 SQL Server 的列存索引方案。最后,我们也会讨论 StoneDB(Tianmu 引擎)中的相关设计及其改进方向。
在进入各项目的具体方案之前,有必要先理解列存为什么”不擅长写”。核心原因可以归结为以下几点:
1. 列拆分带来的写放大
插入一行含 N 列的记录,行存只需一次顺序追加,而列存需要向 N 个不同的物理位置各写入一个值。对于宽表(例如上百列),单行插入的 I/O 次数可能是行存的数十倍。
2. 压缩与修改不可兼得
列存的高压缩比来自于同一列中数据类型和分布的一致性。字典编码、游程编码(RLE)等算法对连续、有序的数据效果极好,但一旦需要在中间插入或修改一个值,整个编码块可能都要重新计算和重写。
3. 排序假设被破坏
许多列存引擎在写入时会按照某个排序键对数据进行物理排序,以便查询时利用稀疏索引快速跳过无关数据。随机插入会破坏这种排序性,要么需要额外的排序开销,要么牺牲查询时的剪枝能力。
4. 元数据维护的开销
列存通常会维护丰富的元数据(min/max 统计信息、Bloom Filter、Zone Map 等)以支持数据裁剪。每次写入都需要更新这些元数据,小批量高频写入的开销不可忽视。
Delta Store 的设计正是为了在不牺牲列存读取优势的前提下,提供一个可接受的写入路径。
C-Store 是 Michael Stonebraker 等人在 2005 年发表的经典论文中提出的列存数据库原型,后来被商业化为 Vertica。C-Store 首次在学术层面系统性地提出了 Delta Store 的设计理念,对后续所有列存系统产生了深远影响。
C-Store 的核心设计将存储分为两个区域:
WS 和 RS 之间存在明确的分工:所有新的插入操作先进入 WS;更新被拆解为”删除旧版本 + 插入新版本”;后台的 Tuple Mover 进程负责将 WS 中积累的数据批量迁移到 RS,在迁移过程中完成排序、压缩等优化操作。
Vertica 在 C-Store 的基础上做了进一步演化。在其 Enterprise 模式中,将 WS 拆分为:
WOS 驻留在内存中,所有写操作首先落入 WOS,积累到一定阈值后再由后台进程批量写入 ROS。Vertica 的查询执行器通过一个 StorageUnion 算子来透明地合并 WOS 和 ROS 的结果,对上层 SQL 完全不可见。
在后来的 EON 模式(存算分离架构)中,Vertica 取消了 WOS,所有写入直接生成 ROS container。这说明在存算分离的云原生架构下,Delta Store 的形态也在不断演化。
SAP HANA 是业界最具代表性的内存列存数据库之一。作为一个同时支持 OLTP 和 OLAP 的 HTAP 系统,它面对的写入压力远大于纯分析型数据库。HANA 因此设计了一套精巧的三级 Delta 架构。
HANA 的列存表由两个核心组件构成:
SAP HANA 的 Delta Store 实际上分为两级:
数据流向为:写操作 → L1-Delta(行存) → L2-Delta(列存) → Main Store(压缩列存)。
Delta Merge 是将 Delta Store 中的数据合并到 Main Store 的过程。HANA 通过一个名为 mergedog 的后台进程来自动触发 Merge 操作,触发条件包括 Delta 内存大小超过阈值、距上次 Merge 的时间间隔、Delta 行数占比等。
Merge 过程本身是资源密集型的——需要约为表大小两倍的内存(旧 Main + 新 Main + Delta),因此 HANA 提供了多种优化策略:
这种三级架构使得 HANA 能够同时支持高并发的事务写入和复杂的分析查询。L1-Delta 的行存格式保证了单行操作的低延迟,L2-Delta 起到了中间过渡的作用,而 Main Store 则为分析查询提供了最优的读取性能。
Apache Kudu 是 Cloudera 开发的列存引擎,其设计目标是在 Hadoop 生态中同时支持快速的分析扫描和低延迟的随机读写。Kudu 的 Delta Store 设计在业界独树一帜,引入了 UNDO/REDO 的双向 Delta 记录。
Kudu 的 Tablet(数据分片)内部包含:
Kudu 的 DiskRowSet 由三个逻辑部分组成:
+--------------+ +-----------+ +--------------+
| UNDO records | <-- | base data | --> | REDO records |
+--------------+ +-----------+ +--------------+
每个 DiskRowSet 拥有独立的 DeltaMemStore 和一组 DeltaFile。
Kudu 的 Delta 记录有一个重要优势:它只记录发生变化的列,而不是整行。如果只更新了一行中的某一小列,Delta 记录只包含该列的值。这避免了读写大列的开销,是相较于 C-Store 和 PostgreSQL 等系统中全行 MVCC 方式的显著改进。
Kudu 的后台维护管理器定期执行两种 Delta Compaction:
ClickHouse 的核心存储引擎 MergeTree 采用了一种不同于传统 Delta Store 的方法。它借鉴了 LSM-Tree 的核心思想,通过不可变的数据分片(Part)和后台异步合并来解决写入问题。
在 ClickHouse 中,每次 INSERT 操作会生成一个独立的、不可变的 Part。每个 Part 内部按主键排序,以列式格式存储,每列对应一个独立的 .bin 文件(Wide 模式)或所有列共存于一个 data.bin 文件(Compact 模式,适用于小于 10MB 的 Part)。
从某种意义上说,每个新生成的 Part 本身就是一个”Delta”——它代表了一批增量数据。
ClickHouse 的后台线程持续地将多个 Part 合并为更大的 Part。由于每个 Part 内部都按相同的主键排序,合并过程可以通过线性归并(single merge pass)高效完成——只需读取两个 Part 的数据并按序交错写入新 Part,不需要随机访问或临时缓冲区。
不同的 MergeTree 引擎变体在合并时执行不同的语义:
ClickHouse 没有独立的 Delta Store 层,这意味着:
这种设计在高吞吐批量写入 + 分析查询的场景下表现出色,但在需要频繁行级更新的场景下存在短板。这也是为什么 ClickHouse 更定位于 OLAP 而非 HTAP。
Microsoft SQL Server 从 2012 年开始引入列存索引(Columnstore Index),并在后续版本中不断演进。SQL Server 的 Delta Store 设计是对”在成熟 OLTP 引擎中集成列存能力”这一工程问题的经典回答。
SQL Server 的聚簇列存索引(Clustered Columnstore Index, CCI)包含两个辅助结构来支持数据修改:
写入的行为取决于批次大小:
当 Delta Store 中的行数达到 1,048,576(约 100 万行),它会被关闭。
后台进程 Tuple Mover 每隔约 5 分钟运行一次,将 CLOSED 状态的 Delta Store 压缩转换为列式 Rowgroup。从 SQL Server 2019 开始,新增了一个后台合并任务来辅助 Tuple Mover,能够自动压缩存在较长时间的小型 OPEN Delta Rowgroup,或合并因大量删除而变得稀疏的 COMPRESSED Rowgroup。
SQL Server 2016 做了一项影响深远的改动:移除了 Delta Store 上的页压缩。此前 Delta Store 使用了页压缩来节省空间,但这占用了宝贵的 CPU 周期,拖慢了数据加载速度。移除压缩后,虽然 Delta Store 占用的空间略有增加,但写入性能获得了显著提升。配合同版本引入的并行数据插入功能,大幅改善了列存索引的数据加载体验。
StoneDB 是一个开源的 MySQL HTAP 数据库,其 Tianmu 列式存储引擎提供分析查询能力。作为本文的特别讨论对象,我们来分析 StoneDB 的 Delta Store 相关设计及其改进空间。
StoneDB V1.0 的 Tianmu 引擎以列式格式将数据组织为固定大小的 Data Pack。在写入路径上,Tianmu 使用了一个基于内存映射文件的 Insert Buffer(TIANMU_INSERT_BUFFER),默认大小为 512MB,作为写入的缓冲区。
数据以列式格式存储并持久化到 RocksDB。写入时,数据首先进入 Insert Buffer,当 Buffer 满或满足其他条件时,再批量刷入底层存储。Tianmu 引擎依赖 Knowledge Grid 技术(维护每个 Data Pack 的统计信息如 min/max、distinct count 等)来优化查询时的数据裁剪。
相较于前述各系统的方案,StoneDB V1.0 的 Delta Store 机制在以下方面存在改进空间:
StoneDB V2.0 的架构 RFC 提出了基于 MySQL 8.0 Secondary Engine 的新方案:
这种架构在本质上将 Delta Store 的问题交给了 InnoDB——它作为行存引擎天然擅长处理事务写入,而 Tianmu 只需关注如何高效地消费 InnoDB 的变更日志并更新自己的列式数据。这与 Oracle Database In-Memory、SQL Server 列存索引等商业方案的思路类似。
| 维度 | C-Store / Vertica | SAP HANA | Apache Kudu | ClickHouse | SQL Server CCI | StoneDB V1.0 |
|---|---|---|---|---|---|---|
| Delta 格式 | 列存(WS) | 行存 L1 + 列存 L2 | 行存 MemRowSet + UNDO/REDO Delta | 不可变 Part(列存) | 行存 B-tree | 内存映射 Insert Buffer |
| 合并触发 | Tuple Mover 后台进程 | mergedog 后台进程,多条件触发 | 后台维护管理器(多种 Compaction) | 后台合并线程 | Tuple Mover(约 5 分钟周期) | Buffer 满 / 定时 |
| 更新支持 | 删除 + 插入 | 直接在 Delta 中修改 | 列级 REDO Delta | 追加新行 + 合并去重 | Delete Bitmap + 新行插入 | 有限支持 |
| MVCC 支持 | 快照隔离 | 行级多版本 | 基于时间戳的 MVCC | Part 级别 | 行级(Delta Store 内) | 有限 |
| 多级缓冲 | WOS(内存)→ ROS(磁盘) | L1 → L2 → Main | MemRowSet → DiskRowSet + DeltaMemStore → DeltaFile | 无(直接生成 Part) | 单级 Delta Store | 单级 Buffer |
| 最佳场景 | 大规模分析 + 批量加载 | HTAP:高并发事务 + 实时分析 | 流式写入 + 分析 | 高吞吐批量写入 + 分析 | OLTP + 实时运营分析 | 分析为主,轻量写入 |
从各项目的实践中,我们可以提炼出 Delta Store 设计的几条核心原则:
1. 读写路径分离是根本
所有成功的方案都遵循一个基本原则:写入路径和读取路径使用不同的数据组织方式。Delta Store 负责吸收写入冲击,主存储保持读取优化的不变性。这种分离让读和写可以各自优化到极致。
2. 异步合并是关键
Delta 到主存储的合并必须是异步的后台操作,不能阻塞前台的读写。合并策略的设计(何时合并、合并多少、如何控制资源消耗)是整个方案中工程复杂度最高的部分。
3. 行存 Delta 未必是最优选择
C-Store/Vertica 的 WS 采用列存,而 SAP HANA 的 L1-Delta 和 SQL Server 的 Delta Store 采用行存。选择取决于目标工作负载:如果主要是单行事务写入,行存 Delta 更高效;如果主要是小批量的列式加载,列存 Delta 可能更适合。
4. Delta 的大小和数量需要精细控制
Delta 太大会拖慢查询(因为查询需要合并 Delta 和主存储的结果);Delta 太小则合并过于频繁,浪费系统资源。SAP HANA 的多条件触发策略(内存大小、行数占比、未提交行比例等)是一个值得参考的设计。
5. 删除和更新比插入更难处理
大多数系统将更新拆解为”删除 + 插入”,通过 Delete Bitmap / Delete Marker 来标记删除。Kudu 的列级 Delta 记录是一个更精细的优化,避免了因为一列的变化而重写整行。
Delta Store 是列式数据库中连接”写入友好”与”读取高效”这两个看似矛盾需求的桥梁。从 C-Store 的学术原型到 SAP HANA 的工业级三级架构,从 Kudu 的 UNDO/REDO 双向 Delta 到 ClickHouse 的 LSM 式 Part 合并,每个系统都根据自身的设计目标和应用场景给出了不同的答案。
对于 StoneDB 而言,V1.0 的 Insert Buffer 方案虽然满足了基本的写入缓冲需求,但在事务处理能力、数据新鲜度、多级缓冲等方面仍有较大的提升空间。V2.0 通过将事务处理交给 InnoDB、Tianmu 专注于分析这一思路,是一个务实的架构选择。但在 Populate 机制的延迟控制、增量更新的效率等方面,仍然可以借鉴 SAP HANA 的多级 Delta 和 Kudu 的列级 Delta 等先进设计。
归根结底,Delta Store 的设计没有银弹。理想的方案取决于系统要解决的核心问题:批量加载还是单行事务?纯分析还是混合负载?内存充裕还是磁盘为主?理解了这些 trade-off,才能在自己的系统中做出最合适的选择。
参考资料