本文将为您介绍 Hologres 中 JSON 和 JSONB 数据类型的语法和使用方法。
JSON 和 JSONB 介绍
近年来随着移动端应用的普及,应用埋点、用户标签计算等场景开始诞生,为了更好地支撑这类场景,越来越多的大数据系统开始使用半结构化格式来存储此类数据,以获得更加灵活的开发和处理。常用的半结构化类型为 JSON 和 JSONB,在实际业务中 JSON 和 JSONB 的区别如下:
-
JSON 储存的是文本格式的数据,JSONB 储存的是 Binary 格式的数据。
-
JSON 插入速度快,查询速度慢,原因是处理函数必须在每次执行时重新解析该数据。JSONB 插入速度慢,而查询速度快,原因是 JSONB 数据被存储在一种分解好的二进制格式中,因为需要做附加的转换,它在输入时要稍慢一些。但是 JSONB 在查询数据时快很多,因为不需要重新解析。
-
JSON 储存的数据是对数据的完整拷贝,会保留源数据的空格、重复键和顺序等,如果一个值中的 JSON 对象包含同一个键超过一次,所有的键、值对都会被保留。而 JSONB 在解析时会删除掉不必要的空格、重复键和数据的顺序等,如果在输入中指定了重复的键,只有最后一个值会被保留。
JSON 和 JSONB 两种 JSON 数据类型,主要的区别之一是效率,具体说明如下:
-
JSON 类型数据存储输入文本的精准拷贝,处理函数在每次执行时必须重新解析该数据。由于 JSON 类型数据存储的是输入文本的准确拷贝,因此可能会保留因为语法使用而存在的空格等内容。如果一个值中的 JSON 对象包含同一个键超过一次,所有的键值对都会被保留( 处理函数会把最后的值当作有效值)。
-
JSONB 类型数据被存储在一种分解好的二进制格式中,因为需要做附加的转换,在输入时要稍慢一些。由于不需要解析,因此在处理时要快很多。JSONB 不保留空格、不保留对象键的顺序并且不保留重复的对象键。如果在输入中指定了重复的键,只有最后一个值会被保留。
使用限制
Hologres 支持 JSON 和 JSONB 两种 JSON 数据类型,在使用时需要注意的事项如下:
-
仅 Hologres V0.9 及以上版本支持 JSON 类型,如果您的实例是 V0.9 以下版本, 请您使用 常见升级准备失败报错 或加入 Hologres 钉钉交流群反馈,详情请参见 如何获取更多的在线支持? 。
-
仅 Hologres V1.1 及以上版本支持 JSONB 类型创建 GIN 索引。
-
仅 Hologres V1.3 及以上版本支持 JSONB 类型开启列式存储,且 JSONB 的列存优化仅能用于列存表,行存表暂不支持,并且至少 1000 条数据才会触发列存优化。
-
Hologres 暂不支持的函数包括如下函数:json_each、jsonb_each、json_each_text、jsonb_each_text、json_extract_path、jsonb_extract_path、jsonb_to_record。
如果您需要使用 jsonb_extract_path 和 json_extract_path 函数可以使用如下等价写法。
-
SELECT json_extract_path( '{"key":{"key1":"key1","key2":"key2"}}'::json , 'key' , 'key1' --json_extract_path函数的等价写法如下 SELECT '{"key":{"key1":"key1","key2":"key2"}}'::json #> '{"key","key1"}'; -
SELECT jsonb_extract_path( '{"key":{"key1":"key1","key2":"key2"}}'::jsonb , 'key' , 'key1' -- jsonb_extract_path函数的等价写法如下 SELECT '{"key":{"key1":"key1","key2":"key2"}}'::jsonb #> '{"key","key1"}';
-
JSON 和 JSONB 操作符
常用操作符
常用的 JSON 和 JSONB 操作符如下表所示:
|
操作符 |
右操作数类型 |
描述 |
操作示例 |
执行结果 |
|
-> |
int |
获得 JSON 数组元素(索引从 0 开始,负整数从末尾开始计)。 |
|
|
|
-> |
text |
通过键获得 JSON 对象域。 |
|
|
|
->> |
int |
以 TEXT 形式获得 JSON 数组元素。 |
|
|
|
->> |
text |
以 TEXT 形式获得 JSON 对象域。 |
|
|
|
#> |
text[] |
获取在指定路径的 JSON 对象。 |
|
|
|
#>> |
text[] |
以 TEXT 形式获取在指定路径的 JSON 对象。 |
|
|
其他操作符
除了常用的 JSON 和 JSONB 操作符之外,也支持如下额外操作符,满足不同业务开发需求。
|
操作符 |
右操作数类型 |
描述 |
操作示例 |
执行结果 |
|
@> |
jsonb |
左侧的 JSON 值是否包含右侧的 JSON 路径或值。 |
|
|
|
<@ |
jsonb |
左侧的 JSON 路径或值是否被包含在右侧的 JSON 值中。 |
|
|
|
? |
text |
键或元素字符串是否存在于 JSON 值中。 |
|
|
|
?| |
text[] |
数组字符串中的任何一个键或元素字符串是否存在于 JSON 值中。 |
|
|
|
?& |
text[] |
是否所有数组字符串都存在于 JSON 值中。 |
|
|
|
|| |
jsonb |
将两个 JSONB 值串接成一个新的 JSONB 值。
说明
|
|
|
|
- |
text |
根据键值从左操作数开始删除键或者值。 |
|
|
|
- |
text[] |
根据键值从左操作数开始删除多个键或者值。 |
|
|
|
- |
integer |
删除指定位置的数组元素(负值表示倒数)。如果 JSON 不是数组则抛出一个错误。 |
|
|
|
#- |
text[] |
删除具有指定路径元素(对于 JSON 数组,负值表示倒数)。 |
|
|
JSON 和 JSONB 函数
处理函数
如下为可以用于处理 JSON 值的函数描述及操作示例。
|
函数 |
返回值 |
描述 |
操作示例 |
执行结果 |
|
json_array_length(json) |
int |
返回最外层 JSON 数组中的元素数量。 |
|
|
|
jsonb_array_length(jsonb) |
||||
|
json_object_keys(json) |
setof text |
返回最外层 JSON 对象中的键集合。 |
|
|
|
jsonb_object_keys(jsonb) |
||||
|
json_populate_record(base anyelement, from_json json) |
anyelement |
扩展 from_json 中的对象成一个行,它的列匹配由 base 定义的记录类型。 |
|
|
|
jsonb_populate_record(base anyelement, from_json jsonb) |
||||
|
json_populate_recordset(base anyelement, from_json json) |
setof anyelement |
扩展 from_json 中最外的对象数组为一个集合,该集合的列匹配由 base 定义的记录类型。 |
|
|
|
jsonb_populate_recordset(base anyelement, from_json jsonb) |
||||
|
json_array_elements(json) |
setof json |
把一个 JSON 数组扩展成一个 JSON 值的集合。 |
|
|
|
jsonb_array_elements(jsonb) |
setof jsonb |
|||
|
json_array_elements_text(json) |
setof text |
把一个 JSON 数组扩展成一个 text 值集合。 |
|
|
|
jsonb_array_elements_text(jsonb) |
||||
|
json_typeof(json) |
text |
把最外层的 JSON 值的类型作为一个文本字符串返回。可能的类型是: object、array、string、number、 boolean 以及 null。 |
|
|
|
jsonb_typeof(jsonb) |
||||
|
json_strip_nulls(from_json json) |
json |
返回 from_json,其中所有具有空值的对象域都被省略。其他空值不动。 |
|
|
|
jsonb_strip_nulls(from_json jsonb) |
jsonb |
|||
|
jsonb_set(target jsonb, path text[], new_value jsonb[,create_missing boolean]) |
jsonb |
返回 target,其中由 path 指定的节点用 new_value 替换,如果 path 指定的项不存在并且 create_missing 为真(默认为 true)则加上 new_value。正如面向路径的操作符一样,出现在 path 中的负整数表示从 JSON 数组的末尾开始数。 |
|
|
|
|
|
|||
|
jsonb_insert(target jsonb, path text[], new_value jsonb, [insert_after boolean]) |
jsonb |
返回被插入了 new_value 的 target。如果 path 指定的 target 节点在一个 JSONB 数组中,new_value 将被插入到目标之前(insert_after 为 false,默认情况)或者之后(insert_after 为真)。如果 path 指定的 target 节点在一个 JSONB 对象内,则只有当 target 不存在时才插入 new_value。对于面向路径的操作符来说,出现在 path 中的负整数表示从 JSON 数组的末尾开始计数。 |
|
|
|
|
|
|||
|
jsonb_pretty(from_json jsonb) |
text |
把 from_json 返回成一段缩进后的 JSON 文本。 |
|
|
|
jsonb_agg |
jsonb |
将值(包括空值)聚合为 JSON 数组。 |
|
|
|
jsonb_object_agg |
jsonb |
将 Key/Value 对聚合为 JSON 对象,值可以为空,但名称不能为空。 |
|
|
|
is_valid_json |
BOOLEAN |
IS_VALID_JSON
函数用于验证
JSON
字符串,如果字符串格式是正确的
JSON
字符串,则该函数返回布尔值
说明
|
|
|
解析函数
|
函数 |
描述 |
操作示例 |
执行结果 |
|
try_cast_to_jsonb(text) |
将 TEXT 类型转换为 JSONB 类型。如果文本字段不符合 JSONB 格式,将返回 NULL 值。
说明
仅 Hologres V2.0.24 及以上版本支持该函数。 |
|
|
|
to_json(anyelement) |
此函数可以将该值返回为 JSON。数组和组合会被(递归)转换成数组和对象,对于不是数组和组合的值,如果有从该类型到 JSON 的造型,造型函数将被用来执行该转换,否则将产生一个标量值。对于任何不是数字、布尔、空值的标量类型,将使用文本表达,使其是一个有效的 JSON 值。 |
|
|
|
to_jsonb(anyelement) |
|||
|
array_to_json(anyarray [, pretty_bool]) |
此函数可以将数组作为一个 JSON 数组返回。一个 PostgreSQL 多维数组会成为一个数组的 JSON 数组。如果 pretty_bool 为真,将在第 1 维度的元素之间增加换行。 |
|
|
|
json_build_array(VARIADIC "any") |
此函数可以从一个可变参数列表构造一个可能包含异质类型的 JSON 数组。 |
|
|
|
jsonb_build_array(VARIADIC "any") |
|||
|
json_build_object(VARIADIC "any") |
此函数可以从一个可变参数列表构造一个 JSON 对象。通过转换,该参数列表由交替出现的键和值构成。 |
|
|
|
jsonb_build_object(VARIADIC "any") |
|||
|
json_object(text[]) |
此函数可以从一个文本数组构造一个 JSON 对象。该数组必须可以是具有偶数个成员的一维数组(成员被当做交替出现的键/值对),或者是一个二维数组(每一个内部数组刚好有 2 个元素,可以被看做是键/值对)。 |
|
|
|
jsonb_object(text[]) |
|
|
|
|
json_object(keys text[], values text[]) |
json_object 的这种形式从两个独立的数组得到键/值对。在其他方面和一个参数的形式相同。 |
|
|
|
jsonb_object(keys text[], values text[]) |
JSONB 索引
从 Hologres V1.1 版本开始,在查询层支持 JSONB 类型的 GIN,BTree 索引加速查询。在 JSONB 上创建 GIN 索引的方式有两种:使用默认的 jsonb_ops 操作符创建和使用 jsonb_path_ops 操作符创建。当前索引都是针对 JSONB 类型,推荐使用 JSONB 类型而不是 JSON 类型。
-
JSONB GIN 索引不支持和 JSONB 列式存储优化一起使用。Hologres V3.0.42、V3.1.10 版本起,如果同时设置,GIN 索引不会生效。
-
JSONB GIN 索引适用稀疏的 JSONB 字段。非稀疏场景建议使用 JSONB 列式存储优化。
-
使用默认的 jsonb_ops 操作符创建索引
CREATE INDEX idx_name ON table_name USING gin (idx_col); -
使用 jsonb_path_ops 操作符创建索引
CREATE INDEX idx_name ON table_name USING gin (idx_col jsonb_path_ops);
两者的区别为:在 jsonb_ops 的 GIN 索引中,JSONB 数据中的每个 key 和 value 都是作为一个单独的索引项的,而 jsonb_path_ops 则只为每个 value 创建一个索引项。
GIN 索引可以通过原生 PostgreSQL 操作符和 Hologres 操作符实现,使用示例如下。
原生 PostgreSQL 操作符
-
创建 jsonb_ops 操作符索引。
--1、创建表 BEGIN; DROP TABLE IF EXISTS json_table; CREATE TABLE IF NOT EXISTS json_table id INT ,j jsonb COMMIT; --2、创建jsonb_ops操作符索引 CREATE INDEX index_json on json_table USING GIN(j); --3、插入数据 INSERT INTO json_table VALUES (1, '{"key1": 1, "key2": [1, 2], "key3": {"a": "b"}}') , (1, '{"key1": 1}'), (2, '{"key2": [1, 2], "key3": {"a": "b"}}') ; --4、查询数据 SELECT * FROM json_table WHERE j ? 'key1'; --返回结果 id | j ----+------------------------------------------------- 1 | {"key1": 1, "key2": [1, 2], "key3": {"a": "b"}} 1 | {"key1": 1}使用
explain命令查看执行计划如下。explain SELECT * FROM json_table WHERE j ? 'key1'; QUERY PLAN Gather (cost=0.00..0.26 rows=1000 width=12) -> Local Gather (cost=0.00..0.23 rows=1000 width=12) -> Decode (cost=0.00..0.23 rows=1000 width=12) -> Bitmap Heap Scan on json_table (cost=0.00..0.13 rows=1000 width=12) Recheck Cond: (j ? 'key1'::text) -> Bitmap Index Scan on index_json (cost=0.00..0.00 rows=0 width=0) Index Cond: (j ? 'key1'::text) Optimizer: HQO version 1.3.0执行计划中出现了
Index Scan步骤,表明查询过程使用了索引。 -
创建 jsonb_path_ops 操作符索引。
--1、创建表 BEGIN; DROP TABLE IF EXISTS json_table; CREATE TABLE IF NOT EXISTS json_table id INT ,j jsonb COMMIT; --2、创建jsonb_ops操作符索引 CREATE INDEX index_json on json_table USING GIN(j jsonb_path_ops); --3、插入数据 INSERT INTO json_table ( SELECT "key1": "'||i||'" ,"key2": "'||i%100||'" ,"key3": "'||i%1000 ||'" ,"key4": "'||i%10000||'" ,"key5": "'||i%100000||'" }')::jsonb FROM generate_series(1, 1000000) i --4、查询包含'{"key1": "10"}'的数据 SELECT * FROM json_table WHERE j @> '{"key1": "10"}'::JSONB; --返回结果 id | j ----+------------------------------------------------------------------------ 10 | {"key1": "10", "key2": "10", "key3": "10", "key4": "10", "key5": "10"} (1 row)使用
explain命令查看执行计划如下。explain SELECT * FROM json_table WHERE j @> '{"key1": "10"}'::JSONB; QUERY PLAN ------------------------------------------------------------------------------------------- Gather (cost=0.00..0.26 rows=1000 width=12) -> Local Gather (cost=0.00..0.23 rows=1000 width=12) -> Decode (cost=0.00..0.23 rows=1000 width=12) -> Bitmap Heap Scan on json_table (cost=0.00..0.13 rows=1000 width=12) Recheck Cond: (j @> '{"key1": "10"}'::jsonb) -> Bitmap Index Scan on index_json (cost=0.00..0.00 rows=0 width=0) Index Cond: (j @> '{"key1": "10"}'::jsonb) Optimizer: HQO version 1.3.0 (8 rows)执行计划中出现了
Index Scan步骤,表明查询过程使用了索引。
Hologres 操作符
由于原生 PostgreSQL 的 JSONB 的 GIN 索引是非精确的索引,所以检索数据后需要进行 recheck 动作。最终导致创建索引后性能不一定提升。针对上述情况,Hologres 实现了一种新的 ops_class,可以省去 recheck 的动作,且若不指定索引操作符,系统会默认使用该操作符,具体使用方式如下。
使用该操作符时,由于最大存储 127 个字节,超过该长度的索引会被截断,所以 JSONB 字段超长时还是会被截断,从而发生 recheck 的操作。是否有 recheck 行为,可以使用 EXPLAIN ANALYZE 语句查看物理执行计划验证。
其中
jsonb_holo_ops
对应
jsonb_ops
,支持
?, ?|, ?&, @>
的过滤操作。其中
jsonb_holo_path_ops
对应
jsonb_path_ops
,仅支持
@>
的过滤操作。
-
创建 jsonb_holo_ops 操作符号索引。
--1、创建表 BEGIN ; DROP TABLE IF EXISTS json_table; CREATE TABLE IF NOT EXISTS json_table id INT ,j jsonb COMMIT ; --2、创建索引,使用jsonb_holo_ops操作符 CREATE INDEX index_json on json_table USING GIN(j jsonb_holo_ops); --3、插入数据 INSERT INTO json_table VALUES (1, '{"key1": 1, "key2": [1, 2], "key3": {"a": "b"}}') , (1, '{"key1": 1}'), (2, '{"key2": [1, 2], "key3": {"a": "b"}}') ; --4、查询 SELECT * FROM json_table WHERE j ? 'key1'; --返回结果 id | j ----+------------------------------------------------- 1 | {"key1": 1} 1 | {"key1": 1, "key2": [1, 2], "key3": {"a": "b"}} (2 rows) -
创建 jsonb_holo_path_ops 操作符号索引。
--1、创建表 BEGIN ; DROP TABLE IF EXISTS json_table; CREATE TABLE IF NOT EXISTS json_table id INT ,j jsonb --2、创建索引,使用jsonb_holo_path_ops操作符 CREATE INDEX index_json on json_table USING GIN(j jsonb_holo_path_ops); --3、插入数据 INSERT INTO json_table ( SELECT "key1": "'||i||'" ,"key2": "'||i%100||'" ,"key3": "'||i%1000 ||'" ,"key4": "'||i%10000||'" ,"key5": "'||i%100000||'" }')::jsonb FROM generate_series(1, 1000000) i --4、查询,筛选包含'{"key1": "10"}'的数据 SELECT * FROM json_table WHERE j @> '{"key1": "10"}'::JSONB ; --返回结果 id | j ----+------------------------------------------------------------------------ 10 | {"key1": "10", "key2": "10", "key3": "10", "key4": "10", "key5": "10"} (1 row)
数据导入示例:通过 Flink 实时导入 JSONB 数据
通过 Flink 将数据导入 Hologres 时,在 Flink 的 SQL 作业中,需要将字段定义为 Flink 的数据类型;只有在 Hologres 中创建内部表时,才需要将字段定义为 Hologres 的数据类型,Flink 与 Hologres 数据类型映射请参见 Blink/Flink 与 Hologres 的数据类型映射 。
如果您期望使用 Flink 将 JSON 数据写入 Hologres,则需要在 Flink SQL 作业中,将源表、结果表中的 JSON 数据字段定义为 VARCHAR 类型,仅在 Hologres 内部表中将其定义为 JSONB 类型,示例如下:
-
Hologres 中创建内部表,将
message字段定义为 JSONB 类型:BEGIN ; DROP TABLE IF EXISTS holo_internal_table; CREATE TABLE IF NOT EXISTS holo_internal_table id BIGINT NOT NULL, message JSONB NOT NULL CALL set_table_property('holo_internal_table', 'distribution_key', 'id'); COMMIT ; -
Flink 作业中将源表、结果表的
message字段定义为 VARCHAR 类型,并写入 Hologres:CREATE TEMPORARY TABLE randomSource ( id BIGINT, message VARCHAR WITH ('connector' = 'datagen'); CREATE TEMPORARY TABLE sink_holo ( id BIGINT, message VARCHAR WITH ( 'connector' = 'hologres', 'dbname'='<yourDBname>', --Hologres的数据库名称。 'tablename'='<holo_internal_table>', --Hologres用于接收数据的表名称。 'username'='<yourUsername>', --当前阿里云账号的AccessKey ID。 'password'='<yourPassword>', --当前阿里云账号的AccessKey Secret。 'endpoint'='<yourEndpoint>', --当前Hologres实例VPC网络的Endpoint。 INSERT INTO sink_holo SELECT '{"k":"v"}' randomSource;
高级调优:列式 JSONB 存储
GIN 索引只是计算层的优化,实际计算时还需要扫描整个 JSON 内容。为了提升 JSONB 数据的查询效率,Hologres 从 V1.3 版本开始支持 JSONB 数据的列式存储,通过存储层的优化,使得 JSONB 数据能够像结构化数据一样按列式存储,有效提高数据的压缩效率,同时也提升 JSONB 数据的查询效率。
JSONB 列式存储详情请参见 列式 JSONB 。