Hologres supports JSON and JSONB as semi-structured data types for storing and querying flexible, schema-less data. This topic covers the differences between the two types, supported operators and functions, GIN indexing, and integration with Realtime Compute for Apache Flink.
JSON vs. JSONB: choosing the right type
Both types store JSON-formatted data but differ in storage format and performance characteristics.
| JSON | JSONB | |
|---|---|---|
| Storage format | Plain text | Decomposed binary |
| Write speed | Fast | Slower (extra conversion on write) |
| Read speed | Slower (reparsed on every query) | Faster (no reparsing needed) |
| Preserves input text exactly | Yes — spaces, key order, and duplicate keys retained | No — spaces removed, keys deduplicated (last value wins), key order not preserved |
| Supports GIN indexes | No | Yes (Hologres V1.1+) |
| Supports column-oriented storage | No | Yes (Hologres V1.3+) |
Recommendation: use JSONB for most workloads. JSONB is faster to query, supports GIN indexes, and supports column-oriented storage. Use JSON only when you need to preserve the exact input text — including key order, spaces, and duplicate keys.
Duplicate key behavior: when the same key appears multiple times in an input value, JSON retains all key-value pairs (processing functions treat the last value as authoritative), while JSONB keeps only the last value.
Limits
| Constraint | Detail |
|---|---|
| JSON data type | Requires Hologres V0.9 or later. |
| JSONB GIN indexes | Requires Hologres V1.1 or later. |
| JSONB column-oriented storage | Requires Hologres V1.3 or later. Supported only for column-oriented tables; row-oriented tables are not supported. Column-oriented storage activates only when the table has 1,000 or more records. |
| GIN indexes + column-oriented storage | Mutually exclusive starting from Hologres V3.0.42 and V3.1.10. When both are enabled, GIN indexes do not take effect. Use column-oriented storage for non-sparse fields. |
| Unsupported functions | json_each, jsonb_each, json_each_text, jsonb_each_text, json_extract_path, jsonb_extract_path, jsonb_to_record |
Workaround for `json_extract_path` and `jsonb_extract_path`: use the #> path operator instead.
-- json_extract_path equivalent
SELECT '{"key":{"key1":"key1","key2":"key2"}}'::json #> '{"key","key1"}';
-- jsonb_extract_path equivalent
SELECT '{"key":{"key1":"key1","key2":"key2"}}'::jsonb #> '{"key","key1"}';For more information about upgrading your instance, see Instance upgrades.
Operators
Commonly used operators
The following six operators work with both JSON and JSONB.
| Operator | Right operand type | Description | Example | Result |
|---|---|---|---|---|
-> | int | Gets a JSON array element by index (0-based; negative counts from end). | select '[{"a":"foo"},{"b":"bar"},{"c":"baz"}]'::json->2 | {"c":"baz"} |
-> | text | Gets a JSON object field by key. | select '{"a": {"b":"foo"}}'::json->'a' | {"b":"foo"} |
->> | int | Gets a JSON array element as text. | select '[1,2,3]'::json->>2 | 3 |
->> | text | Gets a JSON object field as text. | select '{"a":1,"b":2}'::json->>'b' | 2 |
#> | text[] | Gets a JSON object at the specified path. | select '{"a": {"b":{"c": "foo"}}}'::json#>'{a,b}' | {"c":"foo"} |
#>> | text[] | Gets a JSON object at the specified path as text. | select '{"a":[1,2,3],"b":[4,5,6]}'::json#>>'{a,2}' | 3 |
JSONB-only operators
The following operators apply only to JSONB data and support containment checks, key existence tests, concatenation, and deletion.
| Operator | Right operand type | Description | Example | Result |
|---|---|---|---|---|
@> | jsonb | Checks whether the left value contains the right JSON path or value. | select '{"a":1, "b":2}'::jsonb @> '{"b":2}'::jsonb | true |
<@ | jsonb | Checks whether the left JSON path or value is contained in the right value. | select '{"b":2}'::jsonb <@ '{"a":1, "b":2}'::jsonb | true |
? | text | Checks whether a key or element string exists in the JSONB value. | select '{"a":1, "b":2}'::jsonb ? 'b' | true |
?| | text[] | Checks whether any key or element string in the array exists in the JSONB value. | select '{"a":1, "b":2, "c":3}'::jsonb ?| array['b', 'c'] | true |
?& | text[] | Checks whether all strings in the array exist in the JSONB value. | select '["a", "b"]'::jsonb ?& array['a', 'b'] | true |
|| | jsonb | Concatenates two JSONB values. For objects with the same key, the right operand's value is used. This operator is not recursive. | select '["a", "b"]'::jsonb || '["c", "d"]'::jsonb | ["a", "b", "c", "d"] |
- | text | Deletes the matching key or value from the left operand. | select '{"a": "b"}'::jsonb - 'a' | {} |
- | text[] | Deletes multiple matching keys or values from the left operand. | select '{"a": "b", "c": "d"}'::jsonb - '{a,c}'::text[] | {} |
- | integer | Deletes the array element at the specified position (negative counts from end). Returns an error if the value is not an array. | select '["a", "b"]'::jsonb - 1 | ["a"] |
#- | text[] | Deletes the element at the specified path. For arrays, negative integers count from end. | select '["a", {"b":1}]'::jsonb #- '{1,b}' | ["a", {}] |
Functions
Processing functions
| Function | Return type | Description | Example | Result |
|---|---|---|---|---|
json_array_length(json) / jsonb_array_length(jsonb) | int | Returns the number of elements in the outermost JSON array. | select json_array_length('[1,2,3,{"f1":1,"f2":[5,6]},4]') | 5 |
json_object_keys(json) / jsonb_object_keys(jsonb) | setof text | Returns the set of keys in the outermost JSON object. | select json_object_keys('{"f1":"abc","f2":{"f3":"a", "f4":"b"}}') | f1 f2 |
json_populate_record(base anyelement, from_json json) / jsonb_populate_record(base anyelement, from_json jsonb) | anyelement | Expands the object in from_json into a row matching the record type defined by base. If a cast exists, it is applied; otherwise, the value is represented as JSON text. | select * from json_populate_record(null::myrowtype, '{"a": 1, "b": ["2", "a b"], "c": {"d": 4, "e": "a b c"}}') | 1 | ["2", "a b"] | {"d": 4, "e": "a b c"} |
json_populate_recordset(base anyelement, from_json json) / jsonb_populate_recordset(base anyelement, from_json jsonb) | setof anyelement | Expands the outermost array of objects in from_json into a set of rows matching the record type defined by base. | select * from json_populate_recordset(null::myrowtype, '[{"a":1,"b":2},{"a":3,"b":4}]') | 1|2 3|4 |
json_array_elements(json) / jsonb_array_elements(jsonb) | setof json / setof jsonb | Expands a JSON array into a set of JSON values. | select * from json_array_elements('[1,true, [2,false]]') | 1 true [2,false] |
json_array_elements_text(json) / jsonb_array_elements_text(jsonb) | setof text | Expands a JSON array into a set of text values. | select * from json_array_elements_text('["foo", "bar"]') | foo bar |
json_typeof(json) / jsonb_typeof(jsonb) | text | Returns the data type of the outermost JSON value as text. Possible values: OBJECT, ARRAY, STRING, NUMBER, BOOLEAN, NULL. | select json_typeof('-123.4') | number |
json_strip_nulls(from_json json) / jsonb_strip_nulls(from_json jsonb) | json / jsonb | Removes all object fields with null values. Other null values are preserved. | select json_strip_nulls('[{"f1":1,"f2":null},2,null,3]') | [{"f1":1},2,null,3] |
jsonb_set(target jsonb, path text[], new_value jsonb [, create_missing boolean]) | jsonb | Replaces the value at path in target with new_value. If create_missing is true (default) and the path does not exist, new_value is inserted. Negative integers in path count from the end of arrays. | select jsonb_set('[{"f1":1,"f2":null},2,null,3]', '{0,f1}','[2,3,4]', false) | [{"f1":[2,3,4],"f2":null},2,null,3] |
jsonb_insert(target jsonb, path text[], new_value jsonb [, insert_after boolean]) | jsonb | Inserts new_value at path in target. For array paths: inserts before the target element by default (insert_after = false); set true to insert after. For object paths: inserts only if the key does not exist. Negative integers in path count from end. | select jsonb_insert('{"a": [0,1,2]}', '{a, 1}', '"new_value"') | {"a": [0, "new_value", 1, 2]} |
jsonb_pretty(from_json jsonb) | text | Returns from_json as indented, human-readable JSON text. | select jsonb_pretty('[{"f1":1,"f2":null},2,null,3]') | [{ "f1": 1, "f2": null }, 2, null, 3] |
jsonb_agg | jsonb | Aggregates values (including nulls) into a JSON array. | See example below. | — |
jsonb_object_agg | jsonb | Aggregates key-value pairs into a JSON object. Keys cannot be null; values can be null. | See example below. | — |
is_valid_json(text) | boolean | Returns true if the input string is valid JSON; false otherwise. ARRAY values are also valid JSON and can be written to JSON or JSONB columns. Requires Hologres V1.3.12 or later. | See example below. | — |
`jsonb_agg` and `jsonb_object_agg` example:
DROP TABLE IF EXISTS t;
CREATE TABLE t (
k int PRIMARY KEY,
class int NOT NULL,
v text NOT NULL
);
INSERT INTO t (k, class, v)
SELECT
(1 + s.v),
CASE (s.v) < 3
WHEN TRUE THEN 1
ELSE 2
END,
chr(97 + s.v)
FROM generate_series(0, 5) AS s (v);
SELECT
class,
jsonb_agg(v ORDER BY v DESC) FILTER (WHERE v <> 'b') AS "jsonb_agg",
jsonb_object_agg(v, k ORDER BY v DESC) FILTER (WHERE v <> 'e') AS "jsonb_object_agg(v, k)"
FROM t
GROUP BY class;Result:
class | jsonb_agg | jsonb_object_agg(v, k)
-------+-----------------+--------------------------
1 | ["c", "a"] | {"a": 1, "b": 2, "c": 3}
2 | ["f", "e", "d"] | {"d": 4, "f": 6}`is_valid_json` example:
DROP TABLE IF EXISTS test_json;
CREATE TABLE test_json (
id int,
json_strings text
);
INSERT INTO test_json
VALUES (1, '{"a":2}'), (2, '{"a":{"b":{"c":1}}}'), (3, '{"a": [1,2,"b"]}');
INSERT INTO test_json
VALUES (4, '{{}}'), (5, '{1:"a"}'), (6, '[1,2,3]');
SELECT
id,
json_strings,
is_valid_json(json_strings)
FROM test_json
ORDER BY id;Result:
id | json_strings | is_valid_json
----+---------------------+--------------
1 | {"a":2} | true
2 | {"a":{"b":{"c":1}}} | true
3 | {"a": [1,2,"b"]} | true
4 | {{}} | false
5 | {1:"a"} | false
6 | [1,2,3] | trueParsing functions
| Function | Description | Example | Result |
|---|---|---|---|
try_cast_to_jsonb(text) | Converts a text value to JSONB. Returns null if the input is not valid JSONB. Requires Hologres V2.0.24 or later. | select try_cast_to_jsonb('{"key": 1}') select try_cast_to_jsonb('{"key": 1') | {"key": 1} NULL |
to_json(anyelement) / to_jsonb(anyelement) | Returns the value as a JSON object. Arrays and composites are recursively converted. Scalar values use a cast function if available; otherwise, they are represented as JSON text. | select to_json('Fred said "Hi."'::text) | "Fred said \"Hi.\"" |
array_to_json(anyarray [, pretty_bool]) | Returns an array as a JSON array. Multidimensional arrays produce nested JSON arrays. When pretty_bool is true, line feeds are added between top-level elements. | select array_to_json('{{1,5},{99,100}}'::int[]) | [[1,5],[99,100]] |
json_build_array(VARIADIC "any") / jsonb_build_array(VARIADIC "any") | Builds a JSON array from a variable list of arguments. Arguments may be of different types. | select json_build_array(1,2,'3',4,5) | [1, 2, "3", 4, 5] |
json_build_object(VARIADIC "any") / jsonb_build_object(VARIADIC "any") | Builds a JSON object from a variable list of alternating key-value arguments. | select json_build_object('foo',1,'bar',2) | {"foo": 1, "bar": 2} |
json_object(text[]) / jsonb_object(text[]) | Builds a JSON object from a text array. Accepts a one-dimensional array with an even number of members (alternating key-value pairs) or a two-dimensional array where each inner array is a key-value pair. | select json_object('{a, 1, b, "def", c, 3.5}') | {"a": "1", "b": "def", "c": "3.5"} |
json_object(keys text[], values text[]) / jsonb_object(keys text[], values text[]) | Builds a JSON object from two separate arrays of keys and values. | select json_object('{a, b}', '{1,2}') | {"a": "1", "b": "2"} |
JSONB field indexing
Hologres V1.1 and later support GIN indexes and B-tree indexes on JSONB fields to accelerate queries. Index JSONB fields rather than JSON fields — GIN indexes are not available for JSON.
JSONB GIN indexes are suitable for sparse JSONB fields. For non-sparse fields, use JSONB column-oriented storage optimization instead. Starting from Hologres V3.0.42 and V3.1.10, GIN indexes and column-oriented storage cannot be used together — when both are enabled, GIN indexes will not take effect.
Two GIN index operator classes are available:
jsonb_ops(default): creates an index entry for each key and value. Supports?,?|,?&, and@>operators.jsonb_path_ops: creates an index entry for each value only. Supports only the@>operator.
-- Create a GIN index using jsonb_ops (default)
CREATE INDEX idx_name ON table_name USING gin (idx_col);
-- Create a GIN index using jsonb_path_ops
CREATE INDEX idx_name ON table_name USING gin (idx_col jsonb_path_ops);Native PostgreSQL operators
Native PostgreSQL GIN indexes require a data recheck after retrieval, which may limit query performance improvements. The examples below demonstrate both operator classes.
Example: `jsonb_ops` index with key-existence query
-- 1. Create a table.
BEGIN;
DROP TABLE IF EXISTS json_table;
CREATE TABLE IF NOT EXISTS json_table
(
id INT,
j jsonb
);
COMMIT;
-- 2. Create a GIN index using jsonb_ops.
CREATE INDEX index_json ON json_table USING GIN(j);
-- 3. Insert data.
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. Query rows where key1 exists.
SELECT * FROM json_table WHERE j ? 'key1';Result:
id | j
----+-------------------------------------------------
1 | {"key1": 1, "key2": [1, 2], "key3": {"a": "b"}}
1 | {"key1": 1}Run EXPLAIN to confirm the index is used:
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.0The Bitmap Index Scan step confirms the index is used.
Example: `jsonb_path_ops` index with containment query
-- 1. Create a table.
BEGIN;
DROP TABLE IF EXISTS json_table;
CREATE TABLE IF NOT EXISTS json_table
(
id INT,
j jsonb
);
COMMIT;
-- 2. Create a GIN index using jsonb_path_ops.
CREATE INDEX index_json ON json_table USING GIN(j jsonb_path_ops);
-- 3. Insert 1,000,000 rows.
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. Query rows containing '{"key1": "10"}'.
SELECT * FROM json_table WHERE j @> '{"key1": "10"}'::JSONB;Result:
id | j
----+------------------------------------------------------------------------
10 | {"key1": "10", "key2": "10", "key3": "10", "key4": "10", "key5": "10"}
(1 row)Run EXPLAIN to confirm the index is used:
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)Hologres operators
Hologres provides two additional operator classes — jsonb_holo_ops and jsonb_holo_path_ops — that eliminate the data recheck step required by native PostgreSQL GIN indexes, improving query performance.
jsonb_holo_opsandjsonb_holo_path_opssupport index lengths of 1 to 127 bytes. Index values longer than 127 bytes are truncated, and truncated fields require a data recheck. RunEXPLAIN ANALYZEto check whether a recheck occurs.
jsonb_holo_ops: equivalent tojsonb_ops. Supports?,?|,?&, and@>operators.jsonb_holo_path_ops: equivalent tojsonb_path_ops. Supports only the@>operator.
Example: `jsonb_holo_ops` index
-- 1. Create a table.
BEGIN;
DROP TABLE IF EXISTS json_table;
CREATE TABLE IF NOT EXISTS json_table
(
id INT,
j jsonb
);
COMMIT;
-- 2. Create a GIN index using jsonb_holo_ops.
CREATE INDEX index_json ON json_table USING GIN(j jsonb_holo_ops);
-- 3. Insert data.
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. Query rows where key1 exists.
SELECT * FROM json_table WHERE j ? 'key1';Result:
id | j
----+-------------------------------------------------
1 | {"key1": 1}
1 | {"key1": 1, "key2": [1, 2], "key3": {"a": "b"}}
(2 rows)Example: `jsonb_holo_path_ops` index
-- 1. Create a table.
BEGIN;
DROP TABLE IF EXISTS json_table;
CREATE TABLE IF NOT EXISTS json_table
(
id INT,
j jsonb
);
-- 2. Create a GIN index using jsonb_holo_path_ops.
CREATE INDEX index_json ON json_table USING GIN(j jsonb_holo_path_ops);
-- 3. Insert 1,000,000 rows.
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. Query rows containing '{"key1": "10"}'.
SELECT * FROM json_table WHERE j @> '{"key1": "10"}'::JSONB;Result:
id | j
----+------------------------------------------------------------------------
10 | {"key1": "10", "key2": "10", "key3": "10", "key4": "10", "key5": "10"}
(1 row)Import JSONB data from Realtime Compute for Apache Flink
When importing JSON data from Realtime Compute for Apache Flink to Hologres, the field types must match each system's supported types:
Flink source and result tables: define JSON fields as
VARCHAR.Hologres internal tables: define JSON fields as
JSONB.
For the full data type mapping, see Data type mappings between Realtime Compute for Apache Flink and Hologres.
Step 1: Create the Hologres internal table with a JSONB column.
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;Step 2: Create the Flink source and result tables using VARCHAR for the JSON field, then write data to 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 database name
'tablename' = '<holo_internal_table>', -- Target Hologres table
'username' = '<yourUsername>', -- Alibaba Cloud AccessKey ID
'password' = '<yourPassword>', -- Alibaba Cloud AccessKey secret
'endpoint' = '<yourEndpoint>' -- VPC endpoint of your Hologres instance
);
INSERT INTO sink_holo
SELECT
1,
'{"k":"v"}'
FROM randomSource;Column-oriented storage for JSONB data
GIN indexes optimize query performance at the compute layer but still require scanning the full JSON content during execution. Hologres V1.3 and later provide column-oriented storage for JSONB data, which optimizes at the storage layer. JSONB data is stored in columns like structured data, improving compression efficiency and accelerating queries.
For setup and configuration details, see Accelerate JSONB queries.