很多场景中由于业务变化数据结构不能确定,数据对象属性也可能随时间而变化。这时使用json保持动态数据是较好的选择,ClickHouse提供相应的工具支持存储和解析JSON数据。
ClickHouse除了以string存储json,22.3版本开始还提供json对象类型实验特性。

存储JSON数据

最简单方式使用字符串列存储JSON对象,创建示例表:

CREATE TABLE test_string ( `t` DateTime, `v` String )ENGINE = MergeTree ORDER BY t

插入示例数据:

INSERT INTO test_string VALUES(now(), '{"name":"Joe","age":95}')INSERT INTO test_string VALUES(now(), '{"name":"Jack","rating":9}')INSERT INTO test_string VALUES(now(), '{"name":"Tom","rating":8}')

ClickHouse 提供了几个函数处理、验证JSON数据,下面通过分别进行验证。

验证检查key

可以使用 isVAlidJSON函数进行检查:

SELECT v, isValidJSON(v) FROM test_string

返回1、0 标识是否有效:

┌─v──────────────────────────┬─isValidJSON(v)─┐│ {"name":"Joe","age":95}│1 ││ {"name":"Jack","rating":9} │1 ││ {"name":"Tom","rating":8}│1 │└────────────────────────────┴────────────────┘

也可以检查json对象是否包括特定key,这在数据清洗时经常使用:

SELECTJSONHas(v, 'name') AND JSONHas(v, 'rating') AS is_valid,count(*)FROM test_string GROUP BY is_valid

上面SQL检查json是否包含name和rating属性,返回结果显示有两条记录符合条件:

┌─is_valid─┬─count()─┐│0 │ 1 ││1 │ 2 │└──────────┴─────────┘

抽取值

通常我们需要操作json对象的属性值,有多种方式可以实现,首先我们看基于key的抽取函数:

SELECTv,JSONExtract(v, 'name', 'String'),JSONExtract(v, 'rating', 'UInt32')FROM test_stringLIMIT 5// 返回结果┌─v──────────────────────────┬─JSONExtract(v, 'name', 'String')─┬─JSONExtract(v, 'rating', 'UInt32')─┐│ {"name":"Joe","age":95}│ Joe│0 ││ {"name":"Jack","rating":9} │ Jack │9 ││ {"name":"Tom","rating":8}│ Tom│8 │└────────────────────────────┴──────────────────────────────────┴────────────────────────────────────┘

这里name为key(字符串类型),rating为无符号整型。支持抽取标量类型(String, Int/UInt* , Float*),也支持复合数据类型,如Array 、 Tuple、any组合:

SELECT JSONExtract('{"val": [1,2,3,4]}', 'val', 'Array(UInt8)')[2] as item// 返回结果┌─item─┐│2 │└──────┘

还可以使用JSON_VALUE函数,无需考虑数据类型:

SELECTv,JSON_VALUE(v, '$.name') AS nameFROM test_stringLIMIT 5// 返回结果┌─v──────────────────────────┬─name─┐│ {"name":"Joe","age":95}│ Joe││ {"name":"Jack","rating":9} │ Jack ││ {"name":"Tom","rating":8}│ Tom│└────────────────────────────┴──────┘

在看一个复杂示例:

WITH JSONExtract(json, 'Tuple(a UInt32, b UInt32, c Nested(d UInt32, e String))') AS parsed_jsonSELECTJSONExtractUInt(json, 'a') AS a,JSONExtractUInt(json, 'b') AS b,JSONExtractArrayRaw(json, 'c') AS array_c,tupleElement(parsed_json, 'a') AS a_tuple,tupleElement(parsed_json, 'b') AS b_tuple,tupleElement(parsed_json, 'c') AS array_c_tuple,tupleElement(tupleElement(parsed_json, 'c'), 'd') AS `c.d`,tupleElement(tupleElement(parsed_json, 'c'), 'e') AS `c.e`FROM(SELECT '{"a":1,"b":2,"c":[{"d":3,"e":"str_1"}, {"d":4,"e":"str_2"}, {"d":3,"e":"str_1"},{"d":4,"e":"str_1"}, {"d":7,"e":"str_9"}]}' AS json)FORMAT Vertical

首先在了解json结构的情况下,使用JSONExtract函数生成tuple结构,上面代码使用with子句。然后再使用tupleElement函数从tuple中抽取值。当有嵌套结构或数组时,可以嵌套调用tupleElement函数。

json key索引

ClickHouse支持使用函数构建排序键,这样可以在索引中使用JSON抽取函数来优化某些查询:

CREATE TABLE test_index ( `t` Int64, `v` String )ENGINE = MergeTree ORDER BY JSONExtractUInt(v, 'rating')

上面抽取rating属性值作为排序键,从而使得相关查询效率更高:

SELECT count(*) FROM test_indexWHERE JSONExtractUInt(v, 'rating') = 9

上面语句ClickHouse因使用索引使得查询效率很高,相反,如果过滤JSON属性没有索引,会执行全表扫描。相同查询在相同表上,但排序索引不同,结果相差很大,内存占用和使用时间都有极大差异。

这时你可能会有疑问:如何选择独立列或JSON属性?如果你明确知道json需要抽取哪个字段及数据类型,最好使用独立字段代替。这样可以节省空间(使用特定类型代替字符串)和性能提升(无需json抽取负载)。

试验特性——json对象类型

ClickHouse有试验特性JSON类型,现在考虑生产还为时过早,但我们可以先行学习。因为是试样特性,需要修改配置:

SET allow_experimental_object_type = 1

现在可以使用JSON类型创建表:

CREATE TABLE test_json ( `t` DateTime, `v` JSON )ENGINE = MergeTree ORDER BY t

插入演示数据,则可以在查询中直接使用对象符号:

SELECT v.name, v.rating FROM test_json LIMIT 5

返回结果:

SELECT v.name, v.rating FROM test_json LIMIT 5

另外使用json对象类型暂用空间更好,示例场景中比字符串类型减少大约30%,但缺点是插入速度慢,对比插入字符串和JSON对象,将近有10倍差异。无论如何这仍然是实验性的,希望Clickhouse团队能够提供更详细文档并尽快正式发布该特性。

总结

json数据在ClickHouse中就和string字段一样,但可以使用JSON*函数检查并抽取json键值。还可以使用抽取函数作为索引提升查询性能,但最好考虑将它们移动到单独的列中,只留下动态内容存储在JSON列中。
参考:https://altinity.com/blog/clickhouse-json-data-type-version-22-6;https://medium.com/datadenys/working-with-json-in-clickhouse-32e41ac0ff86