Apache Cloudberry™ (Incubating) 是 Apache 软件基金会孵化项目,由 Greenplum 和 PostgreSQL 衍生而来,作为领先的开源 MPP 数据库,可用于建设企业级数据仓库,并适用于大规模分析和 AI/ML 工作负载。
GitHub: https://github.com/apache/cloudberry
文章作者:张文超,Apache Cloudberry 贡献者;整理:酷克数据
面对 AI 应用带来的非结构化数据管理挑战,Apache Cloudberry 引入了一种专门用来存储管理非结构化数据的新的表类型—— Directory Table(目录表)。
01 Tablespace
在介绍 Directory Table 之前,我们首先需要理解数据库中的一个经典概念:Tablespace,它指的是数据库文件存储的本地目录地址,例如 code/base/。
在 PostgreSQL 中,其默认的 Tablespace 是 pg_default,对应的数据目录是 base 目录,可以通过 default_tablespace 的 guc 来修改默认 tablespace。
以对象存储为例:/bucket_name/tablespace-1/;
以 HDFS 为例 :/usr/warehouse/tablespace-1/;
在创建表时,我们可以指定该表使用的 Tablespace。如果不指定,它将使用默认的 Tablespace。而本期我们要讲解的 Directory Table,实际上就是依赖于 Tablespace 来实现的。接下来,我们将通过实际操作来演示 Tablespace 的使用。
02 Directory Table 实现原理

图 1 Directory Table 存储架构
当处理非结构化数据时,我们可以通过执行 CopyFrom 命令或使用 gpdirtableload 工具将其导入到 Directory Table 中。导入的数据将被存储在 Tablespace 中。
如果使用的是 Local Directory Table,数据将存储在 Local Tablespace(本地表空间)中; 而如果使用的是 Remote Directory Table,数据则会存储在 DFS Tablespace(分布式文件系统表空间)中。
Directory Table 主要涉及三个与元数据相关的操作部分,分别是:Index(索引)、Catalog(目录)以及 Schema table(模式表)。
如下图所示,Catalog 中包含了一张名为 pg_directory_table 的系统表。这张系统表包含三个关键字段:DT_OID,用于唯一标识 Directory Table 的对象标识符(OID);Tablespace 的 OID,用于标识表空间;以及 Directory Table 的具体文件路径。

图 2 Catalog 表结构图
Schema table 可以理解为一张普通 Heap 表,专门用于记录 Directory Table 中的层级信息,从而管理 Directory Table 中存储的所有非结构化数据。Schema table 主要包含五个字段:
relative_path,即文件的相对路径,也可以理解为文件在 Directory Table 中的名称;
size,表示文件的大小; upload_time,记录文件上传的时间; MD5,用于文件的完整性校验; 以及 tag,允许用户在上传文件后添加标签,以便更方便地管理和检索这些非结构化数据。

图 3 Schema table 结构图
03 创建 Directory Table

图 4 创建 Directory Table 流程图
Directory Table 的 Schema 表按照 relative path 列进行分布,上传的非结构化数据文件,会基于分布列将数据分布存储到 segment 中。 主键为 relative path,在插入非结构化数据时,relative path 相同的拒绝插入。同时可以加速 Schema 表的查询效率。 Directory Table 的权限检查基于 Schema 表。 创建成功后,会在 pg_directory_table 系统表中插入一条对应 tuple,同时会在对应的 Tablespace 下生成对应目录。 Schema 表禁止除 Update tag 列外的一切 DML 操作。
04 导入 Directory Table

图 5 Copy 导入非结构化数据流向图
下图是关于 Copy 代码的一个流程:首先,系统会判断操作是否通过 CopyFrom 接口发起,并检查所操作的表是否为 Directory Table,若不是 Directory Table,则执行常规的 Copy table 逻辑;若是,则进一步判断其类型为 QD 还是 QE,分别执行发送数据或接收数据的流程。最后,无论执行了哪种类型的操作,都会进行必要的清理工作,以确保资源得到正确释放和状态得到更新。

图 6 Copy 代码流程
QD 具体的执行流程如下:

图 7 Copy 导入 - QD 流程
CdbCopyStart 会执行 buildGang,建立 QD→QE 之间的 connection,通过 pg 中的 libpq 来传输数据。
ReadBinaryData 会从 libpq 中读取二进制数据流。
Md5Sum 会动态计算 md5,直至文件传输完成得到整个文件的 md5。
ComputeTargetSeg 会基于 relative path 分布键计算 tuple 需要发送的 QE。
CdbCopyEnd 会等待 Copy 到 QE 过程的结束,并收集 QE 返回的状态和统计信息。

图 8 Copy 导入 - QE 流程
NextCopyFromExecute 会从 libpq 中读取 QD 发送的数据并存到 TupleTableSlot 中。
Index Tuple Check 会基于主键检查当前 tuple 是否已经在 index 中存在,这里主键为 relative_path,可以避免相同文件名覆盖。
UFileAddCreatePendingEntry 会添加一个 PendingDelete 对象到链表中,类似于 pg 中 pending delete 的机制,该链表会在事务提交将数据文件落盘,在事务回滚时删除数据文件,保证了数据文件的事务性。
UFileOpen/UFileWrite/UFileSync/UFileClose 是数据文件操作的统一接口,通过这些接口可以实现对底层存储介质的透明,如本地存储或者远端对象存储。
那么如何在 Directory Table 中查询数据呢?如下图所示,我们提供了一些 UDF(用户定义函数)来进行查询。

图 9 查询数据流程
QD 调用 Directory Table 的函数。函数执行时,QD 会向 QE 发送查询计划。QE 收到计划后执行它,并调用 FunctionScan 来扫描 schema 表,找到要查询的那条 tuple,并获取具体的文件路径或 URL(本地文件为路径,远端文件为 URL)。
QE 拿到这个路径或 URL 后,会到 Tablespace 找到对应的 Directory Table,然后找到对应的非结构化数据,紧接着给 QD 返回一个 TupleTableSlot,而返回的数据通过 Motion 的 UDP 传输方式,保证了传输的可靠性。
05 删除数据

图 10 删除数据流程
当 QD 调用这个函数时,它会将删除命令发送给 QE。QE 收到命令后,会调用 remove_file segment 函数,执行一个顺序扫描,扫描 schema 表以找到要删除的 tuple,并获取对应的路径或 URL 。
随后,QE 会构造一个 pending delete 对象,并将其添加到 pending delete 链表中,同时在 schema 表中删除对应的 tuple。这个 pending delete 机制在 commit 或 rollback 时决定文件的最终状态。
06 删除 Directory Table
最后,展示最删除 Directory Table 的流程图,这里需要注意:
aclcheck 会对权限进行检查,只有超级用户或者是 Directory Table 的 owner 才有权限执行删除。
heap_drop_with_catalog 会在 pg_class 元数据的 tuple 进行删除。
RemoveDirectoryTableEntry 会将 pg_directory_table 系统表中对应的 tuple 删除。
UFileAddDeletePendingEntry 类似于 UFileAddCreatePendingEntry,在执行删除时会添加一个 PendingDelete 对象到链表中,该链表会在事务提交将数据文件删除,在事务回滚时不做处理。

图 11 删除 Directory Table 的流程
推荐阅读
👇🏻️扫码加入 Apache Cloudberry 交流群👇🏻️





