本文將為您介紹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以下版本,請您使用自助升級或加入HologresDingTalk交流群反饋,詳情請參見如何擷取更多的線上支援?。
僅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操作符
常用的JSON和JSONB操作符如下表所示:
操作符 | 右運算元類型 | 描述 | 操作樣本 | 執行結果 |
-> | int | 獲得JSON數組元素(索引從0開始,負整數從末尾開始計)。 |
|
|
-> | text | 通過鍵獲得JSON對象域。 |
|
|
->> | int | 以TEXT形式獲得JSON數組元素。 |
|
|
->> | text | 以TEXT形式獲得JSON對象域。 |
|
|
#> | text[] | 擷取在指定路徑的JSON對象。 |
|
|
#>> | text[] | 以TEXT形式擷取在指定路徑的JSON對象。 |
|
|
額外的JSON和JSONB操作符
除了常用的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和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_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 i, ('{ "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 i, ('{ "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 1, '{"k":"v"}' FROM randomSource;
進階調優:列式JSONB儲存
GIN索引只是計算層的最佳化,實際計算時還需要掃描整個JSON內容。為了提升JSONB資料的查詢效率,Hologres從 V1.3版本開始支援JSONB資料的列式儲存,通過儲存層的最佳化,使得JSONB資料能夠像結構化資料一樣按列式儲存,有效提高資料的壓縮效率,同時也提升JSONB資料的查詢效率。
JSONB列式儲存詳情請參見列式JSONB。