暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

StarRocks Primary Key:实时更新不可缺少的利器

StarRocks 2023-12-13
2008
迄今为止,分析型数据库已经成为数据驱动决策的重要组成部分。它们通常用于运行大规模、复杂的分析查询,如数据挖掘和机器学习任务。随着数据科学和人工智能的发展,分析型数据库的重要性在持续增加。
随着实时分析的理念推广,数据分析已经不满足于 T+1 这样的数据延迟。用最新鲜的数据实时分析出结果来指导决策已经成为主流。

01




分析型数据库实时数据变更的挑战




由于分析型数据库的设计和优化主要是为了读取大量数据并执行复杂查询,而不是为了处理大量的写入和实时更新,在存储结构、数据分布等方面的特殊设计能够支撑较高的查询性能,却也给实时数据变更带来了不少挑战。
  1. 存储结构很多分析型数据库采用列式存储,这种方式优化了读取性能,但在插入和更新数据时需要更多的开销。因为当一个新的记录插入或者一个旧的记录更新时,可能需要重写整个数据文件。

  2. 索引和预计算分析型数据库通常使用复杂的索引结构和预聚合计算,如位图索引和查询改写。这些优化提高了查询性能,但也增加了数据更新的复杂性和成本。

  3. 数据分布和并行处理在 MPP 架构中,数据被分布在多个节点上,查询可以在这些节点上并行执行。这种分布式处理提高了查询性能,但在数据更新时可能需要跨节点同步,这可能会引入延迟。
因此,尽管一些现代的分析型数据库开始提供一些实时或近实时的数据更新能力,与专为实时更新设计的 OLTP 数据库相比还是有很大的差距。

02




StarRocks 做了哪些改进




StarRocks 自研的 Primary Key 模型在存储结果,索引等方面进行了改进。
  1. 自适应更新模式维持列存的结构下,实现了列模式的更新方式。在整列更新的场景下只重写被更新的列,而不需要整行数据全部重写,极大的降低了数据更新时的 I/O 开销。

  2. 主键索引基于主键的 bitmap 索引结构支持在查询和更新时可以高效的定位到某行数据所在的位置,配合 delete vector 可以实现基于主键数据的高效检索和更新。

  3. 主键索引落盘在一定时间内没有被访问的主键索引会进行落盘操作,降低索引的内存占用。
StarRocks 通过在存储结构,索引方式方面的优化,实现了一个全新的可实时更新的 table format ,Primary Key 模型可以满足 OLTP 数据库实时同步,Kafka 数据实时接入更新等大多数实时分析场景的需求

03




Primary Key 更新原理




Primary Key 模型可以在保证强大的实时更新能力的同时,具备极速的查询性能。那么 StarRocks 的是如何做到的呢?首先,在 AP 数据库系统中通常使用列存作为底层的存储引擎,一个表往往包含多个文件(或 Rowset),每个文件使用列存格式组织(例如 Parquet),并且是不可修改的。在这种组织结构的前提下支持更新,常见的实现方式包括:
  1. Copy on Write. 当一批更新到来后,需要检查其中每条记录跟原来的文件有无冲突(或者说 Key 相同的记录)。对有冲突的文件,重新写一份新的、包含了更新后数据的。这种方式读取时直接读取最新数据文件即可,无需任何合并或者其他操作,查询性能是最优的,但是写入的代价很大,因此适合 T+1 的不会频繁更新的场景,不适合实时更新场景。

  2. Merge on Read.当一批更新到来后,直接排序后以列存或者行存的形式写入新的文件。由于数据在写入时没有做去重或者说冲突检查,就需要在读取时通过 Key 的比较进行 Merge 的方式,合并多个版本的数据,仅保留最新版本的数据返回给查询执行层。这种方式写入的性能最好,实现也很简单,但是读取的性能很差。

  3. Delta Store.当一批更新到来后,通过主键索引,先找到每条记录原来所在的文件和位置(通常是一个整数的行号)。把位置和所做的修改作为一条 Delta 记录,放到跟原文件对应的一个 Delta Store 中。查询时,需要把原始数据和 Delta Store 中的数据进行 Merge。这里牺牲了部分写入性能来换取读取性能。
StarRocks 采用的是 Delete + Insert 的策略,当一批更新到来后,通过主键索引,先找到每条记录原来所在的位置,把该条记录标记为删除,然后把最新数据作为新记录写入新文件。读取时,根据删除标记来将旧版本过期数据过滤掉,留下最新更新后的数据。该策略的好处是,因为无需像 Merge-on-Read 和 Delta Store 模式下进行 Merge,过滤算子可以下推到 Scan 层直接利用各类索引进行过滤减少扫描开销,所以查询性能的提升空间更大。
但是由于 Delete-and-Insert 模式需要引入主键索引以及存储和管理删除标记,在大规模实时更新和查询场景下要想高效率低实现还是非常有挑战的,我们采用了多种创新技术来实现这一目标:
  • 首先,我们通过 roaring bitmap 来管理和存储每一个列存文件对应的删除标记。比如一个文件中有 10000 行数据,其中 200 行被标记删除,则可以使用一个 bitmap 来标记被删除的行。一般都是很稀疏的,使用 roaring bitmap 可以高效存储和操作。roaring bitmap 在存算一体架构下会被存储到 Rocksdb 里,而在存算分离架构下则会被记录到对象存储文件中。同时,roaring bitmap 也会被缓存在内存中以便能够快速访问。
  • 接着,我们引入了主键索引来保存主键到该记录所在位置的映射,用于在数据导入和删除过程中生成列存文件对应的删除标记。我们提供了两种不同的主键索引实现:
    • 全内存索引。索引数据在数据导入进行时按需构建,一段时间没有持续导入时又会释放以节约内存。比较适合对于更新延迟有更高要求的场景。

    • 持久化索引。可以允许把部分索引数据持久化到磁盘以节约内存占用。针对当前主键索引的使用特点,比如批量操作,无并发,不需要范围查询等特点,我们设计了一套类似 LSM 的分层存储和 compaction 的机制的存储引擎,数据在文件中按 hash 分布编码。持久化索引做到了在节约内存的同时,查询和更新性能几乎接近全内存索引。
  • 除了正常的全列更新和删除操作之外,主键模型还支持了部分列更新的能力。针对不同的数据更新场景,我们提供了两种不同的部分列更新实现,在不影响查询性能的同时,尽可能地降低部分列更新的开销,从而能够保证更新的实时性。
    • 行模式。行模式比较适用于小批量的实时更新场景。实现方式是,在数据导入阶段,先生成只包含部分列数据的列存文件,到了事务提交阶段,通过主键索引找到对应行缺失的列数据,并回填到刚才生成的列存文件中,生成完整的列存文件。如下所示,部分列更新想要将<100, John>,<200, Mike>和<300, Casey>更新为<100, Kim>,<200, Kitty>和<300, John>,因此需要从 Column3 中读取对应的缺失列数据,并且将之前的行标记删除。
    • 列模式。列模式适用于大批量的批处理更新场景。实现方式是,通过主键索引找到被更新的记录所在的源列存文件,读取文件中的原始列数据,在和更新数据合并之后生成和源列存文件一一对应的部分列文件,在元数据中建立文件之间的映射。在执行查询时,由于无需做 Merge 操作,因此不会影响查询性能。如下图所示,我们重新生成了对应 Column2 更新之后的 DeltaColumn2 列文件,后续在构建 Iterator 时,会使用DeltaColumn2 替换 Column2。
在行模式下,由于要补齐缺失的列数据,因此读写放大和更新的列占比有关。假设需要更新的列数占比为 C% 则读写放大的倍数为:1/C%
而在列模式下,不需要补齐缺失的列数据,但是需要读取源列存文件中的原始列数据,在和更新数据合并后再写入部分列文件。因此其读写放大和其更新的行占比有关,假设需要更新的行数占比为 R% 则读写放大的倍数为:1/R%
行模式和列模式的部分列更新可以在同一张表中混合使用,因此用户可以根据自己不同的更新场景,灵活选择不同的模式进行更新,从而在不影响查询性能的前提下,获得最佳的更新效率。

04




Primary Key 更新方式




01


upsert

在数据导入时通过对比主键实现数据的插入和更新,以 insert 作为例子在一次写入中,更新一条数据同时插入新的一行数据。其余导入方式类似。

02


partial update

在导入时对部分列进行更新操作,比如对 employee 表中需要对 ID 为 2、3、5、7 的员工修改其年龄将其加 1。
方式一:
需要更新的数据为一份 csv 文件,内容如下:
2,40
3,32
5,32
7,24


通过 http put 的方式将上述文件中的数据更新到 employee 表中对应的值。
方式二:
通过 SQL 的方式也可以达到上述效果:
update employee set age = age+1 where id in (2,3,5,7)

03


conditional update

在时序数据场景,经常会有数据乱序到达的情况。比如一张订单状态在不同的系统中被更新,在这种情况下通常只需要保留最新的一条记录即可。
Primary key 模型支持在导入时通过非 key 列的比较,来实现只保留最新的一行记录。
假设一张订单表如下:
新增的订单记录如下:
4,2023/09/15,AIR,4260.0,2023/09/18 00:21:04 
6,2023/09/16,FOB,19.6,2023/09/18 00:15:47
7,2023/09/17,TRUCK,624.0,2023/09/18 00:41:22
8,2023/09/18,REG AIR,22.0,2023/09/18 10:01:27


其中 orderid 为 4 和 6 的订单 updatetime 要大于原先 order 表中的记录,新记录会覆盖旧记录。orderid 为 7 的 updatetime 小于之前表内的记录值,说明该条数据是延迟到达状态,故 orderid 为 7 的记录不会被更新。同时 orderid 为 8 的记录之前不存在,会直接写入到表中。

04


update

Primary key 模型可以通过以下两种方式实现数据更新
  1. 在 where 条件中使用 from 从句,实现单表或多表关联的数据更新
假设有如下的订单表和运费表,自 2023/09/15 日零点起 SF 快递公司的运费上调 10% 。
则可以通过如下的 SQL 通过 orderid 对 orders 表和 ship_fee 进行 join。关联后通过 shipdate 和 company 两个条件过滤需要更新的行,然后对 fee 字段进行上调操作。
UPDATE ship_fee
SET fee = fee * 1.1  --Increase by 10%
FROM orders
WHERE orders.shipdate >= '2023/09/15'
  AND ship_fee.company = 'SF'
  AND ship_fee.orderid = orders.orderid;


先通过 orders.shipdate 和 ship_fee.company 两个条件分别筛选出两张表中符合条件的记录,然后通过 orderid 进行关联,保留关联后的结果,并更新对应的 fee 字段值。
  1. 使用 CTE
使用 CTE 可以改写上面的例子,方便理解
WITH increase_fee as (
    SELECT * from orders
    WHERE orders.shipdate >= '2023/09/15'
)
UPDATE ship_fee SET fee = fee * 1.1  --Increase by 10%
FROM increase_fee
WHERE ship_fee.orderid = orders.orderid
AND ship_fee.company = 'SF';

关于 StarRocks 

Linux 基金会项目 StarRocks 是数据分析新范式的开创者、新标准的领导者。面世三年来,StarRocks 一直专注打造世界顶级的新一代极速全场景 MPP 数据库,帮助企业构建极速统一的湖仓分析新范式,是实现数字化转型和降本增效的关键基础设施。
StarRocks 持续突破既有框架,以技术创新全面驱动用户业务发展。当前全球超过 300 家市值 70 亿元以上的头部企业都在基于 StarRocks 构建新一代数据分析能力,包括腾讯、携程、平安银行、中原银行、中信建投、招商证券、大润发、百草味、顺丰、京东物流、TCL、OPPO 等,并与全球云计算领导者亚马逊云、阿里云、腾讯云等达成战略合作伙伴。
拥抱开源,StarRocks 全球开源社区飞速成长。目前,已有超过 300 位贡献者,社群用户近万人,吸引几十家国内外行业头部企业参与共建。项目在 GitHub 星数已超 6500 个,成为年度开源热力值增速第一的项目,市场渗透率跻身中国前十名。


文章转载自StarRocks,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论