在上一篇文章中,我们探讨了Hudi的元数据表如何作为一个自管理的多模式索引子系统,重点介绍了其内部架构以及文件、列统计和分区统计索引如何协同工作实现高效的数据跳过。在本文中,我们将深入研究几种处理特定查询模式的专用索引,包括记录索引、二级索引和表达式索引,并了解如何通过异步索引器高效地构建这些索引。
等值匹配:记录索引与二级索引
除了范围查询,查询中也可能包含等值匹配谓词,如 A = X 或 B IN (X, Y, Z)。虽然列统计等数据跳过索引对此也有帮助,但记录级索引更进一步,能够精确定位包含这些值的具体数据文件。
Hudi的多模式索引子系统通过记录索引和二级索引来满足这一需求:
- 记录索引:存储记录键(Record Key)与其所在文件位置的映射关系。
- 二级索引:存储非记录键列的值与其对应记录键的映射关系,从而支持定位到文件。
记录索引位于元数据表的 record_index/ 分区中。你可以为选定的列创建多个二级索引,每个索引都存储在元数据表的一个专用分区(以 secondary_index_ 为前缀)下。
记录索引是一个高性能的通用索引,同时作用于写入和读取路径。它允许Hudi写入器高效地将更新和删除操作路由到Hudi表中对应的文件组。二级索引则利用记录索引来高效查找非记录键列。本节将重点从读取侧说明这两个索引如何优化等值匹配谓词。
查找过程
与数据跳过过程类似,查询引擎会解析等值匹配谓词并将其下推到Hudi集成组件。该组件随后执行索引查找,并返回需要扫描的文件位置。
首先考虑记录索引。当一个查询(如 id = ‘001’)在Hudi表上运行时,且 id 是记录键,引擎将使用记录索引来查找该键的确切文件位置。索引将这些位置返回给查询引擎,引擎据此计划读取执行。这种直接查找通过确保只扫描相关文件位置,显著优化了查询性能。例如,在一个拥有2万个文件组的400 GB合成Hudi表上,对一个记录键进行过滤查询,在使用记录索引后,其执行时间从977秒下降到仅12秒,减少了98%。
当等值过滤条件是 name = ‘foo’ 且 name 不是记录键字段时,情况有所不同。系统将使用为 name 列构建的二级索引进行查找。二级索引中的条目包含了所有 name 值与其对应记录键的映射。由于多个不同记录可能拥有相同的 name 值,查找可能会返回多个记录键。下一步是使用记录索引查找这些返回的记录键,以找到包含它们、需要扫描的文件位置。因此,要使用二级索引,必须启用记录索引。
SQL示例
你可以在创建表时指定 hoodie.metadata.record.index.enable 来为表启用记录索引:
CREATE TABLE trips (
ts BIGINT,
id STRING,
rider STRING,
driver STRING,
fare DOUBLE,
city STRING,
state STRING
) USING hudi
OPTIONS(
primaryKey = ‘id‘,
hoodie.metadata.record.index.enable = ‘true‘ -- 启用记录索引
)
PARTITIONED BY (city, state);
要为特定列创建二级索引,可以使用 CREATE INDEX:
CREATE INDEX driver_idx ON trips (driver); -- 在 `driver` 列上启用二级索引
当你向示例表写入数据时,索引数据会被写入元数据表中的记录索引和二级索引分区,从而在读取时加速查询执行。
表达式索引
查询谓词通常包含对列进行内联转换的表达式,例如 from_unixtime() 或 substring()。这些表达式阻碍了与标准列索引(如列统计或分区统计索引)的直接匹配。为了优化此类查询,Hudi提供了对转换后的列值进行操作的表达式索引。
Hudi目前支持两种类型的表达式索引:
- 列统计类型:存储应用表达式后,转换值的文件级统计信息(最小值、最大值、空值计数、值计数)。
- 布隆过滤器类型:存储基于应用表达式后的转换值构建的文件级布隆过滤器。
每个表达式索引由其类型、使用的表达式和目标列定义,在元数据表中占据一个专用分区,其分区路径以 expr_index_ 前缀标识。
列统计表达式索引的功能类似于标准列统计索引,对数据跳过非常有效。布隆过滤器表达式索引专为等值匹配谓词设计。与提供精确文件位置的记录和二级索引不同,该索引使用布隆过滤器(一种用于快速存在性检查的空间高效数据结构)来裁剪文件。如果布隆过滤器表明目标值肯定不存在,查询规划器就可以跳过该文件。
布隆过滤器表达式索引对于高基数列最为有效,因为“不存在”结果的概率更高,允许跳过更多的文件。对于低基数列,未来计划引入的位图索引将更加高效。
SQL示例
类似于创建二级索引,你可以这样创建一个(列统计类型的)表达式索引:
CREATE INDEX ts_date ON trips
USING column_stats(ts)
OPTIONS(expr='from_unixtime’, format='yyyy-MM-dd’);
这个例子在列 ts 上创建了一个列统计表达式索引,使用 from_unixtime 表达式将纪元时间戳转换为日期字符串,从而支持基于日期的有效数据跳过。
你同样可以创建布隆过滤器表达式索引:
CREATE INDEX bloom_idx_rider ON trips
USING bloom_filters(rider)
OPTIONS(expr='lower’);
这个例子使用 rider 列的小写值构建了一个布隆过滤器表达式索引,优化了对小写骑手名称的谓词匹配。
使用异步索引器高效构建索引
创建新索引可能是一项资源密集型操作,特别是对于大型表和具有高空间复杂度的索引。当通过DDL或写入器配置向大型表添加此类索引时,耗时的索引初始化过程不应阻塞正在进行的读写操作。
为了应对这一挑战,Hudi的索引管理设计有两个关键目标:索引创建不应阻塞并发读写;索引一旦构建,必须能提供截至最新表提交的一致数据。Hudi通过其异步索引功能满足这些要求,该功能在后台构建索引,而不会中断活跃的写入器和读取器。
异步索引过程包括调度和执行两个阶段。首先,调度器创建一个涵盖到最新数据表提交数据的索引计划。接着,执行器从数据表中读取所需的文件组,并将相应的索引数据写入元数据表。在此过程运行时,并发写入器可以继续摄取数据。异步索引执行器将索引数据写入元数据表中目标索引分区的基础文件,而正在进行的写入器则向这些分区追加新的日志文件。Hudi使用冲突解决机制来判断索引操作是否需要因并发写入冲突而重试。
为了管理这种并发性,必须为索引器和数据写入器配置锁提供程序。成功完成后,操作会在Hudi表的时间线上标记为一次已完成的索引提交。
总结与索引选型指南
在本系列文章中,我们探讨了Hudi的索引子系统如何为数据湖仓带来数据库级别的性能。以下是为你的工作负载选择合适的索引的快速指南:
- 文件索引:始终在元数据表中启用——提供表中的分区和文件列表,便于常见的索引过程。
- 列统计与分区统计索引:默认启用,并通过
hoodie.metadata.index.column.stats.column.list 配置仅包含你经常过滤的列。这些索引对于范围谓词和数据跳过至关重要。
- 记录索引:当你需要频繁对记录键进行点查询,或需要使用二级索引时启用。记录索引还能优化Hudi的写入路径,高效路由更新和删除。
- 二级索引:为出现在等值谓词中的非记录键列创建二级索引。每个二级索引都会增加维护开销,因此应专注于高价值列。
- 表达式索引:当查询包含带有内联转换的谓词时使用。对转换值的范围查询选择列统计类型,对高基数列的等值匹配选择布隆过滤器类型。
- 异步索引:在向大型表添加索引时使用。异步索引器在后台构建索引,保持你的写入器和读取器不被阻塞。
所有索引都与数据写入事务性地一同维护,确保一致性而不牺牲性能。元数据表使用HFile格式进行快速点查找,并通过定期压缩保持读取效率。随着Hudi的持续发展,索引子系统也具备可扩展性。即将推出的功能(如用于低基数列的位图索引和用于AI工作负载的向量搜索索引)将进一步扩展其能力。