跳到主要内容

半结构化数据类型

概述

当前 DataType 的不足

  • DataType 是一个枚举类型,我们必须在使用特定类型后进行匹配。例如,如果我们想通过 DataType 创建反序列化器/序列化器,我们应该始终进行匹配。这并不意味着匹配是不必要的。如果我们想为 DataType 添加越来越多的功能,匹配可能会非常繁琐。

  • DataType 表示为枚举类型,我们无法将其用作泛型参数。

  • DataType 可能涉及一些嵌套数据类型,例如 DataType::Struct,但我们把 DataField 放在 DataType 内部,这在逻辑上是不合理的。

  • 难以将属性放入基于枚举的 DataType 中,例如可空属性 #3726 #3769

关于列的概念过多(Series/Column/Array)

  • DataColumn 是一个枚举,包括 Constant(value)Array(Series)
pub enum DataColumn {
// 值的数组。
Array(Series),
// 单个值。
Constant(DataValue, usize),
}
  • SeriesSeriesTrait 的包装
pub struct Series(pub Arc<dyn SeriesTrait>);

概述

半结构化数据类型用于表示无模式的数据格式,如 JSON、XML 等。 为了兼容 Snowflake 的 SQL 语法,我们支持以下三种半结构化数据类型:

  • Variant:一种带标签的通用类型,可以存储任何其他类型的值,包括 ObjectArray
  • Object:用于表示键值对的集合,其中键是非空字符串,值是 Variant 类型的值。
  • Array:用于表示任意大小的密集或稀疏数组,其中索引是非负整数(最多 2^31-1),值是 Variant 类型。

由于 ObjectArray 可以被视为 Variant 的一种类型,以下介绍主要以 Variant 为例。

示例

以下示例展示了如何创建一个包含 VARIANTARRAYOBJECT 数据类型的表,插入并查询一些测试数据。

CREATE TABLE test_semi_structured (
var variant,
arr array,
obj object
);

INSERT INTO test_semi_structured (var, arr, obj)
SELECT 1, array_construct(1, 2, 3)
, parse_json(' { "key1": "value1", "key2": "value2" } ');

INSERT INTO test_semi_structured (var, arr, obj)
SELECT to_variant('abc')
, array_construct('a', 'b', 'c')
, parse_json(' { "key1": [1, 2, 3], "key2": ["a", "b", "c"] } ');


SELECT * FROM test_semi_structured;

+-------+-------------------+----------------------------------------------------+
| var | arr | obj |
+-------+-------------------+----------------------------------------------------+
| 1 | [ 1, 2, 3 ] | { "key1": "value1", "key2": "value2" } |
| "abc" | [ "a", "b", "c" ] | { "key1": [ 1, 2, 3 ], "key2": [ "a", "b", "c" ] } |
+-------+-------------------+----------------------------------------------------+

设计细节

数据存储格式

为了在带有模式的 parquet 格式文件中存储 Variant 类型的值,我们需要对原始的原始值进行一些转换。我们有以下两种选择:

将数据以 JSON 或类似二进制 JSON 的格式存储在一列中

JSON(JavaScript Object Notation)是最常见的半结构化格式,可以表示任意复杂的层次值。它非常适合表示这种半结构化数据。Variant 类型的数据可以以 JSON 格式编码并存储为原始字符串值。 JSON 格式的主要缺点是每次访问都需要对原始字符串进行昂贵的解析,因此有几种优化的类似二进制 JSON 的格式来提高解析速度和单键访问。 例如,MongoDB 和 PostgreSQL 分别使用 BSONjsonb 来存储 JSON 格式的数据。 UBJSON 也是一种兼容的二进制 JSON 格式规范,它可以提供通用兼容性,使用方便,同时速度更快,效率更高。 所有这些二进制 JSON 格式都有更好的性能,唯一的问题是它们缺乏良好的 Rust 实现库。

将数据的每个唯一键存储在子列中

尽管 JSON 格式可以表示任意数据,但实际上,JSON 数据通常由机器生成,因此我们可以预测其模式和结构。 基于这一特性,我们可以将 JSON 数据中的每个唯一键提取并扁平化为多个独立的虚拟子列。

例如,假设我们有一个名为 tweet 的列,并存储以下 JSON 数据:

{"id":1, "date": "1/11", type: "story", "score": 3, "desc": 2, "title": "...", "url": "..."}
{"id":2, "date": "1/12", type: "poll", "score": 5, "desc": 2, "title": "..."}
{"id":3, "date": "1/13", type: "pollop", "score": 6, "poll": 2, "title": "..."}
{"id":4, "date": "1/14", type: "story", "score": 1, "desc": 1, "title": "...", "url": "..."}
{"id":5, "date": "1/15", type: "comment", "parent": 4, "text": "..."}
{"id":6, "date": "1/16", type: "comment", "parent": 1, "text": "..."}
{"id":7, "date": "1/17", type: "pollop", "score": 3, "poll": 2, "title": "..."}
{"id":8, "date": "1/18", type: "comment", "parent": 1, "text": "..."}

此列可以拆分为 10 个虚拟子列:tweet.idtweet.datetweet.typetweet.scoretweet.desctweet.titletweet.urltweet.parenttweet.texttweet.poll。 每个子列的数据类型也可以从值中自动推导出来,然后我们可以自动创建这些子列并插入相应的值。

这种存储格式的主要优点是,在查询数据时不需要解析原始 JSON 字符串,这可以大大加快查询处理速度。 缺点是在插入数据时需要额外的处理,并且每行数据的模式不完全相同。在差异较大的场景中,许多子列数据将为 Null。 为了在各种场景中获得良好的性能和平衡,我们可以参考论文 JSON Tiles 中介绍的优化算法。

从性能的角度来看,更好的解决方案是将数据存储在类似二进制 JSON 的格式中,并将一些频繁查询的唯一键提取为子列。 然而,为了简化开发,我们在第一个版本中使用 JSON 格式。 类似二进制 JSON 的格式和单独存储的子列将在未来的优化版本中采用。

数据类型

在枚举 TypeID 中添加三个新值 VariantVariantArrayVariantObject,分别支持这三种半结构化数据类型。 由于我们现在有一个名为 Array 的值,我们将半结构化的 Array 类型命名为 VariantArray 以区别于它。 为这些类型定义相应的结构,并实现 DataType 特征。 这些类型对应的 PhysicalTypeIDString,JSON 值将被转换为原始字符串进行存储。

pub enum TypeID {
...
Variant
VariantArray
VariantObject
}

pub struct VariantType {}

pub struct VariantArrayType {}

pub struct VariantObjectType {}

对象列

目前 Column 仅针对基本类型实现,自定义结构或枚举(如 serde_json::Value)没有合适的 Column 实现来存储。 定义 ObjectColumnMutableObjectColumn 作为存储自定义数据类型的泛型结构,并分别实现 ColumnMutableColumn 特征。 ObjectType 可以是任何自定义的结构或枚举类型,我们可以通过指定参数为 serde_json::Value 来定义 JsonColumn。 所有 variant 数据将自动转换为 serde_json::Value 并生成一个 JsonColumn。 未来可以轻松支持其他自定义数据类型,如 BitmapColumn。

#[derive(Clone)]
pub struct ObjectColumn<T: ObjectType> {
values: Vec<T>,
}

#[derive(Debug)]
pub struct MutableObjectColumn<T: ObjectType> {
data_type: DataTypeImpl,
pub(crate) values: Vec<T>,
}

type JsonColumn = ObjectColumn<serde_json::Value>;

待办事项

  • 使用更好的存储格式以提高查询性能