為了提升JSONB數(shù)據(jù)的查詢效率,Hologres從 V1.3版本開始支持對于JSONB類型開啟列式存儲優(yōu)化,能夠降低JSONB數(shù)據(jù)的存儲大小并加速查詢。本文將會為您介紹Hologres中列式JSONB的使用。
列式JSONB原理介紹
如下圖所示開啟JSONB列式存儲優(yōu)化后,系統(tǒng)會在底層自動將JSONB的列轉換為強Schema的列式存儲,查詢JSONB中某一個Value時就可以直接命中指定列,從而提升查詢性能。同時因為JSONB中的Value是按列式存儲的,在存儲層可以達到像普通結構化數(shù)據(jù)一樣的存儲和壓縮效率,從而有效降低存儲,實現(xiàn)降本增效。
JSONB列式存儲優(yōu)化功能對JSON類型數(shù)據(jù)不適用,實際使用過程中請不要對JSON類型開啟列式存儲優(yōu)化。
使用限制
僅Hologres V1.3及以上版本支持JSONB類型開啟列式存儲,建議將Hologres實例版本升級至1.3.37及以上版本再開始使用列式JSONB功能,會獲取更好的性能和更優(yōu)的體驗。升級請使用自助升級或加入實時數(shù)倉Hologres交流群申請升級實例,詳情請參見如何獲取更多的在線支持?。
JSONB的列存優(yōu)化僅能用于列存表,行存表暫不支持,并且至少1000條數(shù)據(jù)才會觸發(fā)列存優(yōu)化。
當前僅支持如下操作符的列式存儲優(yōu)化,并且如果查詢中使用不支持的操作符,反而可能會導致查詢性能下降。
操作符
右操作數(shù)據(jù)類型
描述
操作與結果
->
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列存優(yōu)化。
-- 打開xx表的xx列的JSONB列式存儲優(yōu)化
ALTER TABLE <table_name> ALTER COLUMN <column_name> SET (enable_columnar_type = ON);
table_name為表名稱;column_name為列名稱。
打開JSONB列存優(yōu)化后,系統(tǒng)在Compaction時將歷史數(shù)據(jù)都轉為列存,待Compaction完畢即完成歷史數(shù)據(jù)的列存化。
Compaction會消耗系統(tǒng)資源(比如內存),建議該操作在業(yè)務低峰期操作。可以使用
vacuum table_name;
命令強制觸發(fā)Compaction操作,待vacuum命令執(zhí)行完畢,Compaction操作就執(zhí)行完畢了。Compaction完成后新寫入的數(shù)據(jù)會按照列存存儲。
開啟Decimal類型推導
開啟Decimal類型推導前,請確保已開啟JSONB列存優(yōu)化。
Hologres從 V2.0.11版本開始,支持將DECIMAL類型的數(shù)據(jù)進行列存優(yōu)化。例如如下的JSON數(shù)據(jù):
{
"name":"Mike",
"statistical_period":"2023-01-01 00:00:00+08",
"balance":123.45
}
balance
的數(shù)據(jù)在開啟Decimal推導后,也支持按照列存優(yōu)化。開啟方法如下:
-- 打開xx表的xx列的Decimal列存優(yōu)化
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
,則表示已經(jīng)配置成功。
關閉列式JSONB
通過以下命令關閉某張表的某個列的JSONB列存優(yōu)化。
-- 關閉xx表的xx列的JSONB列式存儲優(yōu)化
ALTER TABLE <table_name> ALTER COLUMN <column_name> SET (enable_columnar_type = OFF);
table_name為表名稱;column_name為列名稱。
關閉JSONB列存優(yōu)化后,系統(tǒng)在Compaction時將歷史數(shù)據(jù)都轉為標準的JSONB存儲方式,待Compaction完畢即完成歷史數(shù)據(jù)的轉換。
Compaction會消耗系統(tǒng)資源(比如內存),建議該操作在業(yè)務低峰期操作。可以使用
vacuum table_name;
命令強制觸發(fā)Compaction操作,待vacuum命令執(zhí)行完畢,Compaction操作就執(zhí)行完畢了。Compaction完成后,新寫入的數(shù)據(jù)會按照JSONB格式存儲。
關閉Decimal類型推導
通過以下命令關閉某張表的某個列的Decimal類型列存優(yōu)化推導。
-- 關閉xx表的xx列的Decimal列存優(yōu)化
ALTER TABLE <table_name> ALTER COLUMN <column_name> SET (enable_decimal = OFF);
table_name為表名稱;column_name為列名稱。
Decimal類型推導關閉后,會立刻觸發(fā)Compaction,將原來已經(jīng)列存優(yōu)化后的Decimal類型的數(shù)據(jù)轉換為原有模式。
設置Bitmap索引
在Hologres中,bitmap_columns
屬性指定位圖索引,是數(shù)據(jù)存儲之外的獨立索引結構,以位圖向量結構加速等值比較場景,能夠對文件塊內的數(shù)據(jù)進行快速的等值過濾,適用于等值過濾查詢的場景。Hologres從V2.0版本開始支持對開啟了列存的JSONB設置Bitmap索引。開啟列存JSONB后,系統(tǒng)會解析出int、int[]、bigint、bigint[]、text、text[]、jsonb這7種數(shù)據(jù)類型。開啟Bitmap索引后,系統(tǒng)會對推導成int、int[]、bigint、bigint[]、text、text[]類型的數(shù)據(jù)建立Bitmap索引。
使用語法如下:
call set_table_property('<table_name>', 'bitmap_columns', '[<columnName>{:[on|off]}[,...]]');
參數(shù)說明:
參數(shù) | 說明 |
table_name | 表名稱。 |
columnName | 列名稱。 |
on | 當前字段打開位圖索引。 重要 僅支持開啟了列存的JSONB設置Bitmap索引。 |
off | 當前字段關閉位圖索引。 |
使用示例
創(chuàng)建表。
DROP TABLE IF EXISTS user_tags; -- 創(chuàng)建數(shù)據(jù)表 BEGIN; CREATE TABLE IF NOT EXISTS user_tags ( ds timestamptz, tags jsonb ); COMMIT;
打開
tags
列的JSONB列存優(yōu)化。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,表示已經(jīng)配置成功。
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)
導入數(shù)據(jù)。
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;
(可選)強制觸發(fā)數(shù)據(jù)落盤。
寫入數(shù)據(jù)后,系統(tǒng)會在數(shù)據(jù)落盤時進行JSONB的列存優(yōu)化,為了盡快看到效果,此處使用如下后臺命令,強制觸發(fā)數(shù)據(jù)落盤。
VACUUM user_tags;
樣例查詢。
使用如下SQL查詢
id
為10
的first_name
。SELECT (tags -> 'first_name')::text AS first_name FROM user_tags WHERE (tags -> 'id')::int = 10;
通過執(zhí)行計劃檢查列存優(yōu)化是否使用。
-- 顯示詳細的統(tǒng)計信息 SET hg_experimental_show_execution_statistics_in_explain = ON; -- 查看執(zhí)行計劃 EXPLAIN ANALYZE SELECT (tags -> 'first_name')::text AS first_name FROM user_tags WHERE (tags -> 'id')::int = 10;
結果中有
columnar_access_used
,表示JSONB使用了列存優(yōu)化。針對步驟6中的查詢,還可以對
tags
設置Bitmap索引,以提升針對某個key等值查詢的效率,設置方法如下。call set_table_property('user_tags', 'bitmap_columns', 'tags');
通過執(zhí)行計劃檢查Bitmap索引是否生效。
-- 查看執(zhí)行計劃 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方案對于大部分使用場景都有比較好的優(yōu)化效果,需要注意的是:對于查詢結果需要帶出完整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;
原因在于底層已經(jīng)將JSONB數(shù)據(jù)轉成了列式存儲,所以當需要查詢出完整JSON數(shù)據(jù)的時候,就需要將那些已經(jīng)列式存儲的數(shù)據(jù)再重新拼裝成原來的JSONB格式:
這個步驟就會產生大量的IO以及轉換開銷,如果涉及到的數(shù)據(jù)量很大,列數(shù)又很多,甚至可能成為性能瓶頸,所以此場景下建議不要開啟列式優(yōu)化。
極稀疏的JSONB數(shù)據(jù)
當Hologres列式化JSONB數(shù)據(jù)遇到稀疏的字段時,Hologres會將這部分字段合并至一個叫做holo.remaining
的特殊列中,以此來避免列數(shù)膨脹的問題。所以如果JSONB數(shù)據(jù)包含的都是稀疏字段,比如極端情況下每個字段都只會出現(xiàn)一次,那么列式化將不會起效,因為所有字段都是稀疏的,那么所有字段都會合并至holo.remaining
字段,等于沒有進行列式化,這種情況下不會有查詢性能的提升。
包含復雜嵌套結構的JSONB數(shù)據(jù)
如下JSONB數(shù)據(jù)的根節(jié)點就是一個數(shù)組,且該數(shù)組中存放的是非同構的JSONB數(shù)據(jù),當前Hologres在列式化JSONB數(shù)據(jù)的時候,遇到類似復雜的嵌套結構,會將這部分數(shù)據(jù)退化成一列,所以此JSONB數(shù)據(jù)開啟列式JSONB優(yōu)化,將不會帶來明顯的查詢性能收益。
'[
{"key1": "value1"},
{"key2": 123},
{"key3": 123.01}
]'
列式JSONB最佳實踐
慢查詢診斷
如果開啟列式JSONB后,發(fā)現(xiàn)查詢性能反而比不開啟性能還要差很多,首先排查查詢是否帶出了完整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
更優(yōu)的SQL寫法
將JSONB字段數(shù)據(jù)轉成TEXT格式有不同的寫法,但是使用
->>
操作符的性能會更好,比如要獲取json_data列中的name屬性:--性能更好 SELECT json_data->>'name' FROM tbl; --性能一般 SELECT (json_data->'name')::text FROM tbl;
如果JSON的某個字段中存儲的是TEXT數(shù)組,需要判斷數(shù)組中是否包含特定值,建議使用以下寫法:
SELECT key FROM tbl WHERE jsonb_to_textarray(json_data->'phones') && ARRAY['123456'];
常見問題
開啟列存化后為什么存儲上漲?
開啟列式JSONB優(yōu)化后,原JSONB數(shù)據(jù)中的字段名都不會再存儲了,而只需存儲每個字段對應的具體值,且列式化后每列的數(shù)據(jù)類型都是一樣的,列式存儲能有比較好的數(shù)據(jù)壓縮率,理論上數(shù)據(jù)的存儲空間會有明顯的下降。
但如果JSONB數(shù)據(jù)中的字段比較稀疏,列數(shù)膨脹比較厲害,那么列式化后的每一列都會帶來額外的存儲開銷(列的統(tǒng)計信息、索引等),且如果列式化后每一列的類型都是TEXT類型,壓縮效果就不會很好。所以實際的存儲壓縮效率與實際業(yè)務的數(shù)據(jù)有關(比如稀疏度等),不一定所有的數(shù)據(jù)都有很好的壓縮效果。