semi-structured-data-types
概述
当前 DataType
的不足
-
DataType
是一个枚举类型,我们必须在使用前进行类型匹配。例如,如果我们想通过DataType
创建反序列化器/序列化器,我们总是需要进行匹配。这并不意味着匹配是不必要的。如果我们想向DataType
添加越来越多的函数,匹配可能会非常麻烦。 -
DataType
表示为枚举类型,我们不能将其用作泛型参数。 -
DataType
可能涉及一些嵌套的数据类型,例如DataType::Struct
,但我们将DataField
放在DataType
内部,这在逻辑上是不合理的。 -
难以将属性放入基于枚举的
DataType
中,例如 nullable 属性 #3726 #3769
关于列的太多概念 (Series/Column/Array)
- DataColumn 是一个枚举,包括
Constant(value)
和Array(Series)
pub enum DataColumn {
// Array of values.
Array(Series),
// A Single value.
Constant(DataValue, usize),
}
- Series 是
SeriesTrait
的一个包装器
pub struct Series(pub Arc<dyn SeriesTrait>);
概述
半结构化数据类型用于表示无模式数据格式,例如 JSON、XML 等。 为了与 Snowflake 的 SQL 语法 兼容,我们支持以下三种半结构化数据类型:
Variant
: 一种带标签的通用类型,可以存储任何其他类型的值,包括Object
和Array
。Object
: 用于表示键值对的集合,其中键是非空字符串,值是Variant
类型的值。Array
: 用于表示任意大小的密集或稀疏数组,其中索引是非负整数(最大为 2^31-1),值是Variant
类型。
由于 Object
和 Array
可以被视为 Variant
的一种类型,因此以下介绍主要以 Variant
为例。
示例
以下示例展示了如何创建具有 VARIANT
、ARRAY
和 OBJECT
数据类型的表,插入和查询一些测试数据。
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" ] } |
+-------+-------------------+----------------------------------------------------+
设计细节
数据存储格式
为了将 Variant
类型的值以带 schema 的 parquet
格式文件存储,我们需要对原始值进行一些转换。我们有以下两种选择:
将数据以 JSON 或类 JSON 的二进制格式存储在单个列中
JSON (JavaScript Object Notation) 是最常见的半结构化格式,可以表示任意复杂的层级值。它非常适合表示这种半结构化数据。Variant
类型的数据可以编码为 JSON 格式并存储为原始字符串值。
JSON 格式的主要缺点是每次访问都需要对原始字符串进行昂贵的解析,因此有几种优化的类 JSON 二进制格式可以提高解析速度和单个键的访问速度。
例如,MongoDB 和 PostgreSQL 分别使用 BSON 和 jsonb 来存储 JSON 格式的数据。
UBJSON 也是一种兼容的二进制 JSON 格式规范,它可以提供通用的兼容性,像 JSON 一样易于使用,同时更快更高效。
所有这些二进制 JSON 格式都具有更好的性能,唯一的问题是它们缺乏良好的 Rust 实现库。
将数据的每个唯一键存储在子列中
尽管 JSON 格式可以表示任意数据,但在实践中,JSON 数据通常由机器生成,因此我们可以预测 Shema 和结构。 基于此特性,我们可以提取 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.id
、tweet.date
、tweet.type
、tweet.score
、tweet.desc
、tweet.title
、tweet.url
、tweet.parent
、tweet.text
、tweet.poll
。
每个子列的数据类型也可以从值中自动推断出来,然后我们可以自动创建这些子列并插入相应的值。
这种存储格式的主要优点是在查询数据时不需要解析原始 JSON 字符串,这可以大大加快查询处理速度。 缺点是在插入数据时需要额外的处理,并且每行数据的 schema 并不完全相同。在一些差异很大的场景中,许多子列数据将为 Null。 为了在各种场景中获得良好的性能和平衡,我们可以参考论文 JSON Tiles 中介绍的优化算法。
从性能的角度来看,更好的解决方案是以类 JSON 的二进制格式存储数据,并提取一些经常查询的唯一键作为子列。 但是,为了简化开发,我们在第一个版本中使用 JSON 格式。 类 JSON 的二进制格式和单独存储的子列将在未来的优化版本中采用。
数据类型
向枚举 TypeID
添加三个新值 Variant
、VariantArray
、VariantObject
,分别支持这三种半结构化数据类型。
由于我们现在有一个名为 Array
的值,我们将半结构化的 Array
类型命名为 VariantArray
以区分它。
为这些类型定义相应的结构,并实现 trait DataType
。
这些类型对应的 PhysicalTypeID
是 String
,JSON 值将被转换为原始字符串进行存储。
pub enum TypeID {
...
Variant
VariantArray
VariantObject
}
pub struct VariantType {}
pub struct VariantArrayType {}
pub struct VariantObjectType {}
对象列
目前,Column
仅针对基本类型实现,自定义结构或枚举(如 serde_json::Value
)没有合适的 Column
实现来存储。
定义 ObjectColumn
和 MutableObjectColumn
作为通用结构来存储自定义数据类型,并分别实现 trait Column
和 MutableColumn
。
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>;
TODO
- 使用更好的存储格式来提高查询性能