采用对象级别的方法同一 schema 中的不同对象可以采用不同技术。例如,某些对象最适合使用
String 类型,另一些则最适合使用 Map 类型。请注意,一旦使用了 String 类型,就无需再对 schema 做进一步决策。反过来,也可以在 Map key 中嵌套子对象——包括用 String 表示的 JSON——如下所示:使用 String 类型
String 类型。可以像下面演示的那样,在查询时使用 JSON 函数提取值。
对于使用动态 JSON 的用户来说,采用上述结构化方法处理数据通常并不现实,因为这类 JSON 要么会发生变化,要么其 schema 还不够明确。为了获得最大的灵活性,你可以直接将 JSON 存储为 String,然后再按需使用函数提取字段。这与将 JSON 作为结构化对象处理正好处于两个极端。这种灵活性是有代价的,也带来了明显的缺点——主要是查询语法会更加复杂,同时性能也会下降。
如前所述,对于原始 person 对象,我们无法保证 tags 列的结构。我们插入原始行 (包括 company.labels,目前暂不处理) ,并将 Tags 列声明为 String:
tags 列,并看到该 JSON 已以字符串形式插入:
JSONExtract 函数可用于从该 JSON 中提取值。请看下面这个简单示例:
String 类型的列 tags,也需要提供 JSON 中要提取的路径。对于嵌套路径,需要嵌套调用函数,例如 JSONExtractUInt(JSONExtractString(tags, 'car'), 'year'),它会提取列 tags.car.year。通过函数 JSON_QUERY 和 JSON_VALUE,可以简化嵌套路径的提取。
考虑一种极端情况:在 arxiv 数据集中,我们将整个正文都视为一个 String。
JSONAsString 格式:
JSON_VALUE(body, '$.versions[0].created')。
String 函数比使用索引进行显式类型转换明显更慢 (> 10x) 。上述查询始终需要执行全表扫描并处理每一行。虽然在像这样的小型数据集上,这些查询仍然会很快,但在更大的数据集上,性能会下降。
这种方法的灵活性显然是以性能和语法复杂度为代价的,因此只应将其用于 schema 中高度动态的对象。
简单 JSON 函数
simpleJSON* 函数可能提供更好的性能,主要是因为它们对 JSON 的结构和格式作出了严格假设。具体来说:
- 字段名必须是常量
-
字段名的编码必须保持一致,例如
simpleJSONHas('{"abc":"def"}', 'abc') = 1,但visitParamHas('{"\\u0061\\u0062\\u0063":"def"}', 'abc') = 0 - 字段名在所有嵌套结构中都必须唯一。不区分嵌套层级,匹配时会无差别查找。如果存在多个匹配字段,则使用首次出现的那个。
-
字符串字面量之外不能出现特殊字符,也包括空格。以下内容无效,无法解析。
simpleJSONExtractString 提取 created 键,这是利用了这样一个事实:对于发布日期,我们只需要第一个值。在这种情况下,为了换取性能提升,simpleJSON* 函数的这些限制是可以接受的。
使用 Map 类型
如果对象用于存储任意键,且这些键对应的值大多属于同一类型,可以考虑使用Map 类型。理想情况下,唯一键的数量不应超过几百个。对于包含子对象的对象,也可以考虑使用 Map 类型,前提是这些子对象的类型保持一致。通常,我们建议将 Map 类型用于标记和标签,例如日志数据中的 Kubernetes pod (容器组) 标记。
尽管 Map 为表示嵌套结构提供了一种简单的方法,但它也有一些明显的限制:
- 所有字段都必须是相同的类型。
- 由于这些字段并不是作为列存在的,因此访问子列需要使用特殊的 map 语法。整个对象本身就是一列。
- 访问某个子列时,会加载整个
Map值,也就是所有同级项及其对应的值。对于较大的 Map,这可能会带来明显的性能损耗。
String 键将对象建模为
Map 时,会使用 String 键来存储 JSON 键名。因此,它始终会是 Map(String, T)``,其中 T` 取决于数据。基本类型值
Map 最简单的用法,是对象中的值都属于同一种基本类型。在大多数情况下,这意味着值 T 使用 String 类型。
来看我们前面的人物 JSON示例,其中 company.labels 对象被判定为动态对象。这里很重要的一点是,我们预计只会向该对象添加 String 类型的键值对。因此,我们可以将其声明为 Map(String, String):
request 对象中的这些字段,需要使用 Map 语法,例如:
Map 函数来查询此类数据,详见这里。如果您的数据类型不统一,也可以使用相关函数进行必要的类型强制转换。
对象值
对于包含子对象的对象,也可以考虑使用Map 类型,前提是这些子对象的类型一致。
假设 persons 对象中的 tags 键需要保持一致的结构,也就是每个 tag 的子对象都包含 name 和 time 列。此类 JSON 文档的简化示例如下:
Map(String, Tuple(name String, time DateTime)):
使用 Nested 类型
Nested 类型可用于对很少变动的静态对象进行建模,作为Tuple 和 Array(Tuple) 的替代方案。我们通常建议避免将这种类型用于 JSON,因为它的行为往往容易令人困惑。Nested 的主要优势在于,子列可用于排序键。
下面我们通过一个示例来说明如何使用 Nested 类型对静态对象进行建模。请看下面这条简单的 JSON 日志条目:
request 键声明为 Nested。与 Tuple 类似,需要显式指定其子列。
flatten_nested
设置flatten_nested 用于控制 Nested 类型的行为。
flatten_nested=1
取值为1 (默认值) 时,不支持任意层级的嵌套。采用此值时,可以将嵌套数据结构理解为多个长度相同的 Array 列,这样最容易理解。字段 method、path 和 version 实际上分别对应独立的 Array(Type) 列,但有一个关键约束:method、path 和 version 字段的长度必须相同。 使用 SHOW CREATE TABLE 可以说明这一点:
-
我们需要使用设置
input_format_import_nested_json,将 JSON 作为嵌套结构插入。否则,就必须先将 JSON 展平,即: -
嵌套字段
method、path和version需要以 JSON 数组的形式传递,即:
Array 意味着可能能够利用 Array 函数 的完整功能集,包括 ARRAY JOIN 子句——如果您的列包含多个值,这会非常有用。
flatten_nested=0
这允许任意层级的嵌套,并且嵌套列会保持为单个Tuple 数组——也就是说,它们实际上就等同于 Array(Tuple)。
这是在 Nested 中使用 JSON 的首选方式,而且通常也是最简单的方式。正如下文所示,它只要求所有对象都以列表形式提供。
下面,我们重新创建表并再次插入一行:
-
插入时不需要
input_format_import_nested_json。 -
Nested类型会在SHOW CREATE TABLE中保留。底层实际上,这一列是Array(Tuple(Nested(method LowCardinality(String), path String, version LowCardinality(String)))) -
因此,必须将
request作为数组插入,即:
示例
上述数据的更大示例可在 S3 的公共存储桶中获取:s3://datasets-documentation/http/。
flatten_nested 设为 0。
以下语句会插入 1000 万行,因此执行可能需要几分钟。如有需要,请加上 LIMIT:
使用成对数组
成对数组在将 JSON 表示为 Strings 的灵活性与采用更结构化方法的性能之间取得了平衡。其 schema 较为灵活,因为任何新字段理论上都可以添加到根级别。不过,这也意味着需要使用明显更复杂的查询语法,而且与嵌套结构不兼容。 例如,考虑下列表:JSONExtractKeysAndValues 实现这一点:
indexOf 函数来确定所需键的索引 (该索引应与值的顺序一致) 。这样即可访问 values 数组列,即 values[indexOf(keys, 'status')]。对于 request 列,我们仍需要一种 JSON 解析方法——这里使用的是 simpleJSONExtractString。