主键表
主键表使用 StarRocks 设计的全新存储引擎。它的主要优势在于支持实时数据更新,同时确保复杂即席查询的高效性能。在实时业务分析中,决策可以受益于主键表,它使用最新的数据来实时分析结果,从而可以减轻数据分析中的数据延迟。
主键表的主键具有 UNIQUE 约束和 NOT NULL 约束,用于唯一标识每行数据。如果新数据行的主键值与表中现有数据行的主键值相同,则会发生 UNIQUE 约束冲突。然后,新数据行将替换现有数据行。
- 自 v3.0 起,主键表的排序键与表的主键分离,可以单独指定排序键。因此,提高了表创建的灵活性。
- 自 v3.1 起,StarRocks 共享数据集群支持创建主键表。
- 自 v3.1.4 起,可以在**本地磁盘**中创建和存储持久索引。
- 自 v3.3.2 起,可以在**对象存储**中创建和存储持久索引。
使用场景
主键表可以支持实时数据更新,同时确保高效的查询性能。它适用于以下场景
- 将事务处理系统中的流数据实时导入到 StarRocks 中。 在正常情况下,事务处理系统除了插入操作外,还涉及大量的更新和删除操作。如果您需要将数据从事务处理系统同步到 StarRocks,我们建议您创建一个主键表。然后,您可以使用工具(例如 CDC Connectors for Apache Flink®)将事务处理系统的二进制日志同步到 StarRocks。StarRocks 使用二进制日志来实时添加、删除和更新表中的数据。这简化了数据同步,并比使用采用 Merge-On-Read 策略的 Unique Key 表的查询性能高出 3 到 10 倍。有关更多信息,请参见从 MySQL 实时同步。
- 通过对单个列执行部分更新来联接多个流。在用户画像等业务场景中,最好使用扁平表来提高多维分析性能并简化数据分析师使用的分析模型。这些场景中的上游数据可能来自各种应用,例如购物应用、交付应用和银行应用,或者来自机器学习系统等系统,这些系统执行计算以获得用户的不同标签和属性。主键表非常适合在这些场景中使用,因为它支持对单个列的更新。每个应用或系统只能更新在其自身服务范围内保存数据的列,同时受益于实时数据添加、删除和更新以及高查询性能。
工作原理
Unique Key 表和 Aggregate 表采用 Merge-On-Read 策略。此策略使数据写入简单高效,但需要在数据读取期间在线合并多个版本的数据文件。此外,由于存在 Merge 运算符,谓词和索引无法下推到底层数据,这会严重影响查询性能。
但是,为了平衡实时更新和查询的性能,主键表中的元数据结构和读/写机制与其他类型的表不同。主键表使用 Delete+Insert 策略。此策略通过使用主键索引和 DelVector 来实现。此策略确保在查询期间只需要读取具有相同主键值的记录中的最新记录,这消除了合并多个版本的数据文件的需要。此外,可以将谓词和索引下推到底层数据,这大大提高了查询性能。
主键表中写入和读取数据的整体过程如下
-
数据写入是通过 StarRocks 的内部 Loadjob 实现的,Loadjob 包括一批数据更改操作(Insert、Update 和 Delete)。StarRocks 将相应 Tablet 的主键索引加载到内存中。对于 Delete 操作,StarRocks 首先使用主键索引查找每个数据行的原始位置(数据文件和行号),在 DelVector 中将数据行标记为已删除(DelVector 存储和管理数据加载期间生成的删除标记)。对于 Update 操作,除了在 DelVector 中将原始数据行标记为已删除外,StarRocks 还会将最新的数据行写入新的数据文件,本质上将 Update 转换为 Delete+Insert(如下图所示)。主键索引也会更新以记录更改的数据行的新位置(数据文件和行号)。
-
在数据读取期间,由于各种数据文件中的历史重复记录在数据写入期间已被标记为已删除,因此只需要读取具有相同主键值的最新数据行。不再需要在网上读取多个版本的数据文件以对数据进行去重并查找最新数据。当扫描底层数据文件时,过滤器运算符和各种索引有助于减少扫描开销(如下图所示)。因此,可以显着提高查询性能。与 Unique Key 表的 Merge-On-Read 策略相比,主键表的 Delete+Insert 策略可以帮助将查询性能提高 3 到 10 倍。
更多细节
如果您想更深入地了解如何将数据写入或从主键表中读取,您可以探索以下详细的数据写入和读取过程
StarRocks 是一个使用列式存储的分析数据库。具体来说,表中的 Tablet 通常包含多个 Rowset 文件,每个 Rowset 文件的数据实际上存储在段文件中。段文件以列式格式组织数据(类似于 Parquet),并且是不可变的。
当要写入的数据分布到 Executor BE 节点时,每个 Executor BE 节点都会执行 Loadjob。Loadjob 包括一批数据更改,可以被视为具有 ACID 属性的事务。Loadjob 可以分为两个阶段:写入和提交。
- 写入阶段:数据根据分区和 Bucket 信息分布到相应的 Tablet。当 Tablet 接收到数据时,数据以列式格式存储,然后形成一个新的 Rowset。
- 提交阶段:所有数据成功写入后,FE 会发起对所有涉及的 Tablet 的提交。每个提交都携带一个版本号,表示 Tablet 数据的最新版本。提交过程主要包括搜索和更新主键索引,将所有更改的数据标记为已删除,基于标记为已删除的数据创建 DelVector,以及为新版本生成元数据。
在数据读取期间,元数据用于根据最新的 Tablet 版本查找需要读取的 Rowset。当正在读取 Rowset 中的段文件时,还会检查其最新版本的 DelVector,这可以确保只需要读取最新的数据,并避免读取具有相同主键值的旧数据。此外,下推到 Scan 层的过滤器运算符可以直接利用各种索引来减少扫描开销。
-
Tablet:表根据分区和 Bucket 机制分为多个 Tablet。它是实际的物理存储单元,并作为副本分布在不同的 BE 上。
-
元数据:元数据存储 Tablet 的版本历史记录和有关每个版本的信息(例如,包括哪些 Rowset)。每个 Loadjob 或 Compaction 的提交阶段都会生成一个新版本。
-
主键索引:主键索引存储由这些主键值标识的数据行与这些数据行的位置之间的映射。它被实现为一个 HashMap,其中键表示编码的主键值,值表示数据行的位置(包括
rowset_id
、segment_id
和rowid
)。通常,主键索引仅在数据写入期间用于查找由特定主键值标识的每个数据行所在的 Rowset 和行。 -
DelVector:DelVector 存储每个 Rowset 中每个段文件(列式文件)的删除标记。
-
Rowset:Rowset 是一个逻辑概念,存储 Tablet 中一批数据更改中的数据集。
-
段:Rowset 中的数据实际上被分段并存储在一个或多个段文件(列式文件)中。每个段文件包含列值和与列相关的索引信息。
用法
创建主键表
您只需要在 CREATE TABLE
语句中定义主键即可创建主键表。例子
CREATE TABLE orders1 (
order_id bigint NOT NULL,
dt date NOT NULL,
user_id INT NOT NULL,
good_id INT NOT NULL,
cnt int NOT NULL,
revenue int NOT NULL
)
PRIMARY KEY (order_id)
DISTRIBUTED BY HASH (order_id)
;
由于主键表仅支持哈希分桶作为分桶策略,您还需要使用 DISTRIBUTED BY HASH ()
定义哈希分桶键。
但是,在实际业务场景中,创建主键表时,通常会使用数据分布和排序键等附加功能来加速查询并更有效地管理数据。
例如,订单表中的 order_id
字段可以唯一标识数据行,因此 order_id
字段可以用作主键。
自 v3.0 起,主键表的排序键与表的主键分离。因此,您可以选择经常用作查询过滤条件的列来形成排序键。例如,如果您经常根据订单日期和商家这两个维度的组合来查询产品销售业绩,则可以使用 ORDER BY (dt,merchant_id)
子句将排序键指定为 dt
和 merchant_id
。
请注意,如果您使用数据分布策略,则主键表当前要求主键包含分区和 Bucket 列。例如,数据分布策略使用 dt
作为分区列,使用 merchant_id
作为哈希 Bucket 列。主键还需要包括 dt
和 merchant_id
。
总而言之,上述订单表的 CREATE TABLE 语句如下
CREATE TABLE orders2 (
order_id bigint NOT NULL,
dt date NOT NULL,
merchant_id int NOT NULL,
user_id int NOT NULL,
good_id int NOT NULL,
good_name string NOT NULL,
price int NOT NULL,
cnt int NOT NULL,
revenue int NOT NULL,
state tinyint NOT NULL
)
PRIMARY KEY (order_id,dt,merchant_id)
PARTITION BY date_trunc('day', dt)
DISTRIBUTED BY HASH (merchant_id)
ORDER BY (dt,merchant_id)
PROPERTIES (
"enable_persistent_index" = "true"
);
主键
表的主键用于唯一标识该表中的每一行。构成主键的一个或多个列在 PRIMARY KEY
中定义,并具有 UNIQUE 约束和 NOT NULL 约束。
请注意以下有关主键的注意事项
- 在 CREATE TABLE 语句中,必须在其他列之前定义主键列。
- 主键列必须包括分区和 Bucket 列。
- 主键列支持以下数据类型:数字(包括整数和 BOOLEAN)、字符串和日期(DATE 和 DATETIME)。
- 默认情况下,编码主键值的最大长度为 128 字节。
- 创建表后无法修改主键。
- 出于数据一致性的目的,无法更新主键值。
主键索引
主键索引用于存储主键值与由主键值标识的数据行的位置之间的映射。通常,仅在数据加载期间(涉及一批数据更改)才将相关 Tablet 的主键索引加载到内存中。您可以综合评估查询和更新的性能要求以及内存和磁盘后,考虑持久化主键索引。
- 持久主键索引
- 完全内存主键索引
当 enable_persistent_index
设置为 true
(默认值)时,可以将主键索引持久化到磁盘。在加载期间,只有一小部分主键索引会加载到内存中,而大部分都存储在磁盘上,以避免占用过多内存。通常,具有持久主键索引的表的查询和更新性能与具有完全内存主键索引的表的查询和更新性能几乎相同。
如果磁盘是 SSD,建议将其设置为 true
。如果磁盘是 HDD 并且加载频率不高,您也可以将其设置为 true
。
自 v3.1.4 起,在 StarRocks 共享数据集群中创建的主键表支持将索引持久化到本地磁盘。从 v3.3.2 开始,StarRocks 共享数据集群进一步支持将索引持久化到对象存储。您可以通过将表属性 persistent_index_type
设置为 CLOUD_NATIVE
来启用此功能。
当 enable_persistent_index
设置为 false
时,主键索引不会持久化到磁盘,也就是说,主键索引完全存储在内存中。在加载期间,与加载的数据相关的 Tablet 的主键索引将加载到内存中,这可能会导致更高的内存消耗。(如果 Tablet 长时间没有加载数据,其主键索引将从内存中释放。)
使用完全内存主键索引时,建议您在设计主键时遵循以下准则,以控制主键索引的内存使用量
- 必须正确设计主键列的数量和总长度。我们建议您识别数据类型占用内存较少的列,并将这些列定义为主键,例如 INT 和 BIGINT,而不是 VARCHAR。
- 在创建表之前,我们建议您根据主键列的数据类型和表中的行数来估算主键索引占用的内存。这样,您可以防止内存不足。以下示例说明如何计算主键索引占用的内存
-
假设将 DATE 数据类型(占用 4 个字节)的
dt
列和 BIGINT 数据类型(占用 8 个字节)的id
列定义为主键。在这种情况下,主键的长度为 12 字节。 -
假设该表包含 10,000,000 行热数据,并存储在三个副本中。
-
鉴于上述信息,根据以下公式,主键索引占用的内存为 945 MB:
(12 + 9) x 10,000,000 x 3 x 1.5 = 945 (MB)
在前面的公式中,
9
是每行的不可变开销,1.5
是每个哈希表的平均额外开销。
-
具有完全内存主键索引的主键表适用于主键占用的内存可控的场景。例子
-
该表包含快速变化的数据和慢速变化的数据。快速变化的数据在最近几天内经常更新,而慢速变化的数据很少更新。假设您需要将 MySQL 订单表实时同步到 StarRocks 以进行分析和查询。在此示例中,表的数据按天分区,并且大多数更新都是对最近几天内创建的订单执行的。历史订单在完成后不再更新。当您运行数据加载作业时,历史订单的主键索引不会加载到内存中。只有最近更新的订单的主键索引才会加载到内存中。
如下图所示,表中的数据按天分区,最近两个分区中的数据经常更新。
-
该表是一个由数百或数千列组成的扁平表。主键仅占表数据的一小部分,并且仅消耗少量内存。例如,用户状态或配置文件表由大量列组成,但只有数千万到数亿用户。在这种情况下,主键消耗的内存量是可控的。
如下图所示,该表仅包含少量行,并且该表的主键仅占表的一小部分。
排序键
从 v3.0 开始,主键表将排序键与主键分离。排序键由 ORDER BY
中定义的列组成,并且可以包含任何列组合,只要列的数据类型满足排序键的要求。
在数据加载期间,数据在按照排序键排序后存储。排序键还用于构建前缀索引以加速查询。建议适当地设计排序键以形成可以加速查询的前缀索引。
- 如果指定了排序键,则前缀索引基于排序键构建。如果未指定排序键,则前缀索引基于主键构建。
- 创建表后,您可以使用
ALTER TABLE ... ORDER BY ...
来更改排序键。不支持删除排序键,也不支持修改排序列的数据类型。
更多内容
- 要将数据加载到创建的表中,您可以参考加载概述以选择合适的加载选项。
- 如果您需要更改主键表中的数据,您可以参考通过加载更改数据或使用 DML (INSERT、UPDATE 和 DELETE)。
- 如果您想进一步加速查询,您可以参考查询加速。
- 如果您需要修改表结构,您可以参考ALTER TABLE。
- 一个AUTO_INCREMENT列可以用作主键。