1. 引言:列存数据库在实时化浪潮中的核心挑战

列式存储因其“读友好,写不友好”的固有特性,与分析型处理(AP)数据库的需求高度契合。通过将每列数据连续存储,列存极大地提升了压缩率和I/O效率,使得面向海量数据的复杂分析查询(OLAP)得以高效执行。在传统的数据仓库模型中,数据更新通常以小时级(T+1h)甚至天级(T+1d)的批处理方式进行,这完美地规避了列存的写入短板。

然而,随着业务实时化需求的日益增长,AP数据库被要求具备实时分析其上游事务型(TP)数据库持续产生的变更数据的能力。这直接将列存“写不友好”的特性推向了风口浪尖。本文旨在系统性地剖析列存更新所面临的核心难点,并以此为基础,深入探讨业界为应对此挑战所演化出的四类主流高效更新技术。

1.1 列存更新的两大根本性难题
  1. 写放大(Write Amplification):此处的写放大特指I/O层面的放大。

    • 传统列存:更新一行数据可能需要重写多个列文件,即使通过攒批写入,对于宽表(上百甚至上千列),I/O开销依然巨大。

    • 行列混存(如 PAX, ORC, Parquet):虽将一行的所有列数据组织在同一数据块(Block)内,并通过块级I/O优化了磁盘写入次数,但在内存层面,将行式数据转换为块内的列式排布仍存在内存操作的放大。

  2. 原地更新(In-Place Update)的不可行性:

    • 高昂的重组成(Reorganization)本:列存数据块为追求高压缩率通常很大(数MB至数十MB)。即便只更新其中一个字段,也可能破坏编码和压缩结构,触发整个大块的重写,造成严重的写放大。

    • 架构限制:现代AP数据库普遍采用计算存储分离的共享存储(Share-Storage)架构,其底层存储(如 HDFS, S3)本身是不可变(Immutable)或仅支持追加(Append-Only)的对象存储系统,从根本上杜绝了原地更新的可能性。

因此,所有现代列存更新技术都必须基于读出-修改-写回(Out-of-Place Update) 的模式。

2. 基线方案:低效的File-Level COW

要理解“高效”,必先审视“低效”。最基础的Out-of-Place更新方案是文件级写时复制(File-Level Copy-on-Write, COW)

  • 核心机制:无论更新一行还是一批数据,都将包含这些数据的整个原始文件完整地复制一份,在副本上应用变更,然后生成一个全新的文件来替代旧文件。

  • 业界代表:Apache Hudi COW表。

  • 实现简述:

    • 并发控制:为实现快照隔离(Snapshot Isolation),Hudi COW表采用文件级乐观锁(Optimistic Concurrency Control, OCC)。事务在修改文件时不加锁,在最终提交(Commit)时检查是否有其他并发事务已经修改并提交了相同的文件。若有冲突,则当前事务回滚(Abort)。这决定了它不支持对同一文件的并发写入。

    • 数据读取:读取操作非常纯粹,只需根据查询的快照版本,选择对应版本的完整文件即可,无任何合并开销。

  • 评估:

    • 优点:极致的读取性能,读写完全分离。

    • 缺点:巨大的写放大,极低的写并发度。此方案无法应对高频、低延迟的实时更新需求。

3. 高效更新技术的设计框架与分类

高效更新技术的核心目标,是在降低写放大提升写并发这两个维度上进行优化,同时尽可能地减少对读取性能的损害

为实现这一目标,业界的技术演进遵循两大路径:

  1. 追求更细的更新粒度:从文件级(File-level)向块级(Block-level)、元组级(Tuple-level / Row-level),乃至字段级(Field-level)演进,以最小化每次更新所需重写的数据量。

  2. 追求更细的并发粒度:从文件级锁(File-level Lock)向元组级锁(Tuple-level Lock)演进,以支持更高的并发写入能力。

基于“并发粒度”和“更新粒度”这两个维度,我们将业界主流的高效更新方案划分为以下四类。

4. 四类高效更新方案深度解析

4.1 第一类:表级并发 + 元组级更新 (Table-level Concurrency + Tuple-level Update)
  • 设计哲学:绝对地重视大批量数据更新(Batch Update)的吞吐量,而牺牲并发更新和单行更新的性能。

  • 业界代表:Apache Iceberg MOR表。

  • 实现简述:

    • 存储架构:由存储主要数据的列存“数据文件”(Data File)和存储删除标记的“删除文件”(Delete File)构成。系统会定期将两者合并(Compact)成新的Data File。

    • 更新流程:更新操作被分解DELETE + INSERT。首先向Delete File中追加一条删除标记,然后将新版本数据作为一个新行写入某个Data File。写入的数据量约等于一个元组,因此是元组级更新

    • 删除标记类型:

      1. 位置删除 (Position Delete):记录被删除行在原Data File中的物理行号。写入时需要先查询行号,但读取时合并效率高(按有序行号进行归并)。

      2. 等式删除 (Equation Delete):记录删除行的筛选条件(如 id=100)。写入时无需预查询,尤其适合基于非主键的批量删除;但读取时合并开销大,需为每一行计算是否匹配多个删除条件。

    • 并发控制:采用表级乐观锁。事务完成时,需要将新产生的Data File和Delete File注册到表的中央元数据中。若提交时发现元数据已被其他事务抢先修改,则当前事务回滚。

  • 评估:Iceberg通过牺牲并发性(表锁)换取了极低的写放大,并通过提供两种Delete File类型,将读写性能的权衡选择权交给了用户,设计十分精妙。

4.2 第二类:文件级并发 + 元组级更新 (File-level Concurrency + Tuple-level Update)
  • 设计哲学:同样重视批量更新,但相比第一类,愿意在并发性上做出让步,以支持比表锁更高的并发度。

  • 业界代表:Apache Hudi MOR表。

  • 实现简述:

    • 存储架构:由存储基础数据的列存“基线文件”(Base File)和存储增量变更的行存“日志文件”(Log File)构成。行存的Log File对频繁写入更友好。

    • 更新流程:更新操作同样分解DELETE + INSERT,但删除标记(记录主键)和新版本数据均被追加写入到与Base File对应的Log File中。写入量也是元组级更新

    • 并发控制:与Hudi COW相同,采用文件级乐观锁,并发度高于Iceberg。

    • 读取流程:需要将Base File与对应的Log File进行合并。由于Log File内数据按追加顺序存储,合并通常实现为基于主键的哈希连接(Hash Join)。这导致即使查询只命中Base File的一小部分,也可能需要扫描整个Log File。

  • 评估:Hudi MOR的并发度优于Iceberg。其基于主键的删除标记在Upsert场景下写入效率高于Iceberg的位置删除(无需预读)。但在读取性能上,基于Hash Join的合并通常劣于Iceberg基于有序行号的归并。两者在设计上各有千秋,无绝对优劣。

4.3 第三类:元组级并发 + 字段级更新 (Tuple-level Concurrency + Field-level Update)
  • 设计哲学:极致地重视单行更新的低延迟和高并发,同时极度节约存储空间。

  • 业界代表:Apache Kudu(自成一派)。

  • 实现简述:

    • 存储架构:写入和更新分离处理。新写入数据进入内存中MemRowSet,刷盘后成为列存DiskRowSet。更新数据则进入内存中DeltaMemStore,刷盘后成为对DiskRowSetREDO records文件DiskRowSet内部嵌有主键的B-Tree索引以加速点查。

    • 更新流程:根据主键通过B-Tree索引快速定位到行号(RowID),然后DeltaMemStore中只记录该行号以及被修改的字段值

    • 并发控制:采用行锁,实现了真正的元组级并发

    • 更新粒度:由于只记录变更的字段,是所有方案中写放大最小的字段级更新

  • 评估:Kudu的方案在写并发和写放大上均做到了极致优化,尤其在大宽表的部分列更新场景下优势巨大。其主要权衡点在于,行锁机制对于超大规模的全表批量更新可能会带来较高的加锁开销。

4.4 第四类:“无限”并发 + 元组级更新 (Unlimited Concurrency + Tuple-level Update)
  • 设计哲学:为了追求极致的写入和更新性能,选择牺牲事务的隔离性,允许数据异常(Anomaly)的发生。

  • 术语澄清:“无限并发”并非指技术上的无限,而是指系统不提供锁或冲突检测机制来保证并发更新的事务性。并发的更新请求会被直接接受,通常遵循“后来者覆盖”(Last Writer Wins)的原则,这可能导致更新丢失(Lost-Update) 异常。

  • 业界代表 1:Apache Doris (Unique Key模型)

    • 架构与流程:其本质是一个LSM-Tree结构。更新操作被视为一次新的写入,系统会为每行记录附加一个序列号(Sequence Number)作为版本号。

    • 读取流程:读取时需要合并LSM-Tree的多个层级,以找出同一主键下版本号最新的记录。

    • 评估:写入(Upsert)速度极快,且天然保证了主键的唯一性。主要缺点是读取时需要合并多层数据,导致聚合等算子难以完全下推,限制了读取的并行度。更重要的是,它默认不支持并发更新的事务性。

  • 业界代表 2:阿里云AnalyticDB (ADB)

    • 架构与流程:采用“磁盘列存文件(Detail File)+ 内存删除位图(In-memory Delete Bitmap)”的架构。更新操作分解DELETE + INSERT:在内存位图中将被删除的行对应位置1,然后将新数据追加写入文件。

    • 评估:内存位图使得删除标记的读写性能极高。但缺点是内存开销巨大。相比Doris MOR,ADB的架构允许文件级的并行读取,因为每个文件都可以通过各自的位图独立完成数据过滤。

  • 该方案的演进:

    • Hologres:将ADB的内存位图思想改进为磁盘位图(On-disk Bitmap),使用Roaring Bitmap编码,并存入一个全局的LSM-Tree中,以降低内存消耗。

    • Doris (Merge-on-Write模型):在Hologres的基础上,为解决读取时需要合并多个版本位图的开销,增加了缓存机制,将合并后的位图结果缓存起来复用。

5. 总结与展望

下表对本文讨论的各类更新方案进行了系统性总结:

| 分类 | 典型代表 | 特点与适用场景 |

| ------------------------------------ | -------------- | -------------------------------------------------------- |

| 低效方案 (File-level COW) | Hudi COW | 读性能最佳。写放大巨大,并发度低。适用于写少读多的准实时分析场景。 |

| 高效方案 1 (Table-level Concurrency) | Iceberg MOR | 批量更新友好。并发更新能力差。适合以大规模批处理为主,少量并发为辅的场景。 |

| 高效方案 2 (File-level Concurrency) | Hudi MOR | 批量更新友好,并发度适中。在批量写入和并发性之间取得了较好的平衡。 |

| 高效方案 3 (Tuple-level Concurrency) | Kudu | 单行/并发更新、部分列更新最佳。写放大最小,并发度最高。适合高并发、低延迟的点更新场景。 |

| 高效方案 4 (Unlimited Concurrency) | Doris MOR, ADB | 写入/更新吞吐量最高。牺牲了事务性,可能丢失更新。适合对数据一致性要求不严苛,但追求极致写入性能的场景。 |

总而言之,列式存储的高效更新不存在“银弹”。每一种方案都是在写放大、写并发、读性能与事务性之间进行精妙权衡的结果。选择何种技术,完全取决于具体的业务负载(Workload)和对数据一致性的要求。