很多场景中由于业务变化数据结构不能确定,数据对象属性也可能随时间而变化。这时使用json保持动态数据是较好的选择,ClickHouse提供相应的工具支持存储和解析JSON数据。
ClickHouse除了以string存储json,22.3版本开始还提供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数据,下面通过分别进行验证。
可以使用 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,这在数据清洗时经常使用:
SELECT
JSONHas(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的抽取函数:
SELECT
v,
JSONExtract(v, 'name', 'String'),
JSONExtract(v, 'rating', 'UInt32')
FROM test_string
LIMIT 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函数,无需考虑数据类型:
SELECT
v,
JSON_VALUE(v, '$.name') AS name
FROM test_string
LIMIT 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_json
SELECT
JSONExtractUInt(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函数。
ClickHouse支持使用函数构建排序键,这样可以在索引中使用JSON抽取函数来优化某些查询:
CREATE TABLE test_index (
`t` Int64,
`v` String
)ENGINE = MergeTree
ORDER BY JSONExtractUInt(v, 'rating')
上面抽取rating属性值作为排序键,从而使得相关查询效率更高:
SELECT count(*) FROM test_index
WHERE JSONExtractUInt(v, 'rating') = 9
上面语句ClickHouse因使用索引使得查询效率很高,相反,如果过滤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