為了提升JSONB資料的查詢效率,Hologres從 V1.3版本開始支援對於JSONB類型開啟列式儲存最佳化,能夠降低JSONB資料的儲存大小並加速查詢。本文將會為您介紹Hologres中列式JSONB的使用。
列式JSONB原理介紹
如下圖所示開啟JSONB列式儲存最佳化後,系統會在底層自動將JSONB的列轉換為強Schema的列式儲存,查詢JSONB中某一個Value時就可以直接命中指定列,從而提升查詢效能。同時因為JSONB中的Value是按列式儲存的,在儲存層可以達到像普通結構化資料一樣的儲存和壓縮效率,從而有效降低儲存,實現降本增效。
JSONB列式儲存最佳化功能對JSON類型資料不適用,實際使用過程中請不要對JSON類型開啟列式儲存最佳化。
使用限制
僅Hologres V1.3及以上版本支援JSONB類型開啟列式儲存,建議將Hologres執行個體版本升級至1.3.37及以上版本再開始使用列式JSONB功能,會擷取更好的效能和更優的體驗。升級請使用自助升級或加入即時數倉Hologres交流群申請升級執行個體,詳情請參見如何擷取更多的線上支援?。
JSONB的列存最佳化僅能用於列存表,行存表暫不支援,並且至少1000條資料才會觸發列存最佳化。
當前僅支援如下操作符的列式儲存最佳化,並且如果查詢中使用不支援的操作符,反而可能會導致查詢效能下降。
操作符
右操作資料類型
描述
操作與結果
->
text
通過鍵獲得JSON對象域。
操作樣本:
select '{"a": {"b":"foo"}}'::json->'a'
返回結果:
{"b":"foo"}
->>
text
以TEXT形式獲得JSON對象域。
操作樣本:
select '{"a":1,"b":2}'::json->>'b'
返回結果:
2
列式JSONB使用
開啟列式JSONB
通過以下語句對某張表的某個JSONB列開啟JSONB列存最佳化。
-- 開啟xx表的xx列的JSONB列式儲存最佳化
ALTER TABLE <table_name> ALTER COLUMN <column_name> SET (enable_columnar_type = ON);
table_name為表名稱;column_name為列名稱。
開啟JSONB列存最佳化後,系統在Compaction時將歷史資料都轉為列存,待Compaction完畢即完成歷史資料的列存化。
Compaction會消耗系統資源(比如記憶體),建議該操作在業務低峰期操作。可以使用
vacuum table_name;
命令強制觸發Compaction操作,待vacuum命令執行完畢,Compaction操作就執行完畢了。Compaction完成後新寫入的資料會按照列存儲存。
開啟Decimal類型推導
開啟Decimal類型推導前,請確保已開啟JSONB列存最佳化。
Hologres從 V2.0.11版本開始,支援將DECIMAL類型的資料進行列存最佳化。例如如下的JSON資料:
{
"name":"Mike",
"statistical_period":"2023-01-01 00:00:00+08",
"balance":123.45
}
balance
的資料在開啟Decimal推導後,也支援按照列存最佳化。開啟方法如下:
-- 開啟xx表的xx列的Decimal列存最佳化
ALTER TABLE <table_name> ALTER COLUMN <column_name> SET (enable_decimal = ON);
table_name為表名稱;column_name為列名稱。
查看某張表的列式JSONB開啟情況
通過以下語句查看某張表的列式JSONB開啟情況。
Hologres V1.3.37及以上版本支援如下命令。
說明該命令在Hologres V2.0.17及以下版本只支援看
public
Schema下的表,從2.0.18版本開始支援查看其他Schema下的表開啟情況。--2.0.17及以下版本僅支援查看public schema的表,2.0.18版本可以查詢其他schema的表 SELECT * FROM hologres.hg_column_options WHERE schema_name='<schema_name>' AND table_name = '<table_name>';
其中schema_name為Schema名稱,table_name為表名稱。
Hologres V1.3.10~V1.1.36版本使用如下命令。
SELECT DISTINCT a.attnum as num, a.attname as name, format_type(a.atttypid, a.atttypmod) as type, a.attnotnull as notnull, com.description as comment, coalesce(i.indisprimary,false) as primary_key, def.adsrc as default, a.attoptions FROM pg_attribute a JOIN pg_class pgc ON pgc.oid = a.attrelid LEFT JOIN pg_index i ON (pgc.oid = i.indrelid AND i.indkey[0] = a.attnum) LEFT JOIN pg_description com on (pgc.oid = com.objoid AND a.attnum = com.objsubid) LEFT JOIN pg_attrdef def ON (a.attrelid = def.adrelid AND a.attnum = def.adnum) WHERE a.attnum > 0 AND pgc.oid = a.attrelid AND pg_table_is_visible(pgc.oid) AND NOT a.attisdropped AND pgc.relname = '<table_name>' ORDER BY a.attnum;
其中table_name為表名稱。
樣本返回結果。
返回結果可以看到某個列的attoptions或option屬性為
enable_columnar_type = ON
,則表示已經配置成功。
關閉列式JSONB
通過以下命令關閉某張表的某個列的JSONB列存最佳化。
-- 關閉xx表的xx列的JSONB列式儲存最佳化
ALTER TABLE <table_name> ALTER COLUMN <column_name> SET (enable_columnar_type = OFF);
table_name為表名稱;column_name為列名稱。
關閉JSONB列存最佳化後,系統在Compaction時將歷史資料都轉為標準的JSONB儲存方式,待Compaction完畢即完成歷史資料的轉換。
Compaction會消耗系統資源(比如記憶體),建議該操作在業務低峰期操作。可以使用
vacuum table_name;
命令強制觸發Compaction操作,待vacuum命令執行完畢,Compaction操作就執行完畢了。Compaction完成後,新寫入的資料會按照JSONB格式儲存。
關閉Decimal類型推導
通過以下命令關閉某張表的某個列的Decimal類型列存最佳化推導。
-- 關閉xx表的xx列的Decimal列存最佳化
ALTER TABLE <table_name> ALTER COLUMN <column_name> SET (enable_decimal = OFF);
table_name為表名稱;column_name為列名稱。
Decimal類型推導關閉後,會立刻觸發Compaction,將原來已經列存最佳化後的Decimal類型的資料轉換為原有模式。
設定Bitmap索引
在Hologres中,bitmap_columns
屬性指定位元影像索引,是資料存放區之外的獨立索引結構,以位元影像向量結構加速等值比較情境,能夠對檔案塊內的資料進行快速的等值過濾,適用於等值過濾查詢的情境。Hologres從V2.0版本開始支援對開啟了列存的JSONB設定Bitmap索引。開啟列存JSONB後,系統會解析出int、int[]、bigint、bigint[]、text、text[]、jsonb這7種資料類型。開啟Bitmap索引後,系統會對推導成int、int[]、bigint、bigint[]、text、text[]類型的資料建立Bitmap索引。
使用文法如下:
call set_table_property('<table_name>', 'bitmap_columns', '[<columnName>{:[on|off]}[,...]]');
參數說明:
參數 | 說明 |
table_name | 表名稱。 |
columnName | 列名稱。 |
on | 當前欄位開啟位元影像索引。 重要 僅支援開啟了列存的JSONB設定Bitmap索引。 |
off | 當前欄位關閉位元影像索引。 |
使用樣本
建立表。
DROP TABLE IF EXISTS user_tags; -- 建立資料表 BEGIN; CREATE TABLE IF NOT EXISTS user_tags ( ds timestamptz, tags jsonb ); COMMIT;
開啟
tags
列的JSONB列存最佳化。ALTER TABLE user_tags ALTER COLUMN tags SET (enable_columnar_type = ON);
查看JSONB列式儲存開啟情況。
select * from hologres.hg_column_options where table_name = 'user_tags';
如下返回結果中可以看到tags行的options屬性是enable_columnar_type = on,表示已經配置成功。
schema_name | table_name | column_id | column_name | column_type | notnull | comment | default | options -------------+------------+-----------+-------------+--------------------------+---------+---------+---------+--------------------------- public | user_tags | 1 | ds | timestamp with time zone | f | | | public | user_tags | 2 | tags | jsonb | f | | | {enable_columnar_type=on} (2 rows)
匯入資料。
INSERT INTO user_tags (ds, tags) SELECT '2022-01-01 00:00:00+08' , ('{"id":' || i || ',"first_name" :"Sig", "gender" :"Male"}')::jsonb FROM generate_series(1, 10001) i;
(可選)強制觸發資料落盤。
寫入資料後,系統會在資料落盤時進行JSONB的列存最佳化,為了儘快看到效果,此處使用如下後台命令,強制觸發資料落盤。
VACUUM user_tags;
範例查詢。
使用如下SQL查詢
id
為10
的first_name
。SELECT (tags -> 'first_name')::text AS first_name FROM user_tags WHERE (tags -> 'id')::int = 10;
通過執行計畫檢查列存最佳化是否使用。
-- 顯示詳細的統計資訊 SET hg_experimental_show_execution_statistics_in_explain = ON; -- 查看執行計畫 EXPLAIN ANALYZE SELECT (tags -> 'first_name')::text AS first_name FROM user_tags WHERE (tags -> 'id')::int = 10;
結果中有
columnar_access_used
,表示JSONB使用了列存最佳化。針對步驟6中的查詢,還可以對
tags
設定Bitmap索引,以提升針對某個key等值查詢的效率,設定方法如下。call set_table_property('user_tags', 'bitmap_columns', 'tags');
通過執行計畫檢查Bitmap索引是否生效。
-- 查看執行計畫 EXPLAIN ANALYZE SELECT (tags -> 'first_name')::text AS first_name FROM user_tags WHERE (tags -> 'id')::int = 10;
返回結果如下:
結果中有
bitmap_used
,表示使用了Bitmap索引。
列式JSONB不推薦的使用情境
使用列式JSONB不僅會降低儲存,還會顯著提升查詢效率。但是列式JSONB並不是所有情境都適用,以下情境不建議使用,否則會事倍功半。
查詢會帶出完整JSONB列
Hologres的列式JSONB方案對於大部分使用情境都有比較好的最佳化效果,需要注意的是:對於查詢結果需要帶出完整JSONB列的情境,效能相較於直接儲存原始格式的JSONB會有降低,比如以下SQL:
--建表DDL
CREATE TABLE TBL(key int, json_data jsonb);
SELECT json_data FROM TBL WHERE key = 123;
SELECT * FROM TBL limit 10;
原因在於底層已經將JSONB資料轉成了列式儲存,所以當需要查詢出完整JSON資料的時候,就需要將那些已經列式儲存的資料再重新拼裝成原來的JSONB格式:
這個步驟就會產生大量的IO以及轉換開銷,如果涉及到的資料量很大,列數又很多,甚至可能成為效能瓶頸,所以此情境下建議不要開啟列式最佳化。
極稀疏的JSONB資料
當Hologres列式化JSONB資料遇到稀疏的欄位時,Hologres會將這部分欄位合并至一個叫做holo.remaining
的特殊列中,以此來避免列數膨脹的問題。所以如果JSONB資料包含的都是稀疏欄位,比如極端情況下每個欄位都只會出現一次,那麼列式化將不會起效,因為所有欄位都是稀疏的,那麼所有欄位都會合并至holo.remaining
欄位,等於沒有進行列式化,這種情況下不會有查詢效能的提升。
包含複雜嵌套結構的JSONB資料
如下JSONB資料的根節點就是一個數組,且該數組中存放的是非同構的JSONB資料,當前Hologres在列式化JSONB資料的時候,遇到類似複雜的嵌套結構,會將這部分資料退化成一列,所以此JSONB資料開啟列式JSONB最佳化,將不會帶來明顯的查詢效能收益。
'[
{"key1": "value1"},
{"key2": 123},
{"key3": 123.01}
]'
列式JSONB最佳實務
慢查詢診斷
如果開啟列式JSONB後,發現查詢效能反而比不開啟效能還要差很多,首先排查查詢是否帶出了完整JSONB列,如果SQL過於複雜,可以使用Explain Analyze
方式來診斷,SQL命令樣本如下:
CREATE TABLE TBL(key int, json_data json); --建表DDL
ALTER TABLE TBL ALTER COLUMN json_data SET (enable_columnar_type = on);
Explain Analyze SELECT json_data FROM TBL WHERE key = 123;
Explain Analyze的結果中會有Hint的資訊,如果在Hint資訊中有以下內容則代表查詢帶出了完整的JSONB列,導致了效能的退化:
Column 'json_data' has enabled columnar jsonb, but the query scanned the entire Jsonb value
更優的SQL寫法
將JSONB欄位資料轉成TEXT格式有不同的寫法,但是使用
->>
操作符的效能會更好,比如要擷取json_data列中的name屬性:--效能更好 SELECT json_data->>'name' FROM tbl; --效能一般 SELECT (json_data->'name')::text FROM tbl;
如果JSON的某個欄位中儲存的是TEXT數組,需要判斷數組中是否包含特定值,建議使用以下寫法:
SELECT key FROM tbl WHERE jsonb_to_textarray(json_data->'phones') && ARRAY['123456'];
常見問題
開啟列存化後為什麼儲存上漲?
開啟列式JSONB最佳化後,原JSONB資料中的欄位名都不會再儲存了,而只需儲存每個欄位對應的具體值,且列式化後每列的資料類型都是一樣的,列式儲存能有比較好的資料壓縮率,理論上資料的儲存空間會有明顯的下降。
但如果JSONB資料中的欄位比較稀疏,列數膨脹比較厲害,那麼列式化後的每一列都會帶來額外的儲存開銷(列的統計資訊、索引等),且如果列式化後每一列的類型都是TEXT類型,壓縮效果就不會很好。所以實際的儲存壓縮效率與實際業務的資料有關(比如稀疏性等),不一定所有的資料都有很好的壓縮效果。