All Products
Search
Document Center

Hologres:JSON and JSONB

Last Updated:Mar 26, 2026

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.

JSONJSONB
Storage formatPlain textDecomposed binary
Write speedFastSlower (extra conversion on write)
Read speedSlower (reparsed on every query)Faster (no reparsing needed)
Preserves input text exactlyYes — spaces, key order, and duplicate keys retainedNo — spaces removed, keys deduplicated (last value wins), key order not preserved
Supports GIN indexesNoYes (Hologres V1.1+)
Supports column-oriented storageNoYes (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

ConstraintDetail
JSON data typeRequires Hologres V0.9 or later.
JSONB GIN indexesRequires Hologres V1.1 or later.
JSONB column-oriented storageRequires 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 storageMutually 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 functionsjson_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.

OperatorRight operand typeDescriptionExampleResult
->intGets a JSON array element by index (0-based; negative counts from end).select '[{"a":"foo"},{"b":"bar"},{"c":"baz"}]'::json->2{"c":"baz"}
->textGets a JSON object field by key.select '{"a": {"b":"foo"}}'::json->'a'{"b":"foo"}
->>intGets a JSON array element as text.select '[1,2,3]'::json->>23
->>textGets 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.

OperatorRight operand typeDescriptionExampleResult
@>jsonbChecks whether the left value contains the right JSON path or value.select '{"a":1, "b":2}'::jsonb @> '{"b":2}'::jsonbtrue
<@jsonbChecks whether the left JSON path or value is contained in the right value.select '{"b":2}'::jsonb <@ '{"a":1, "b":2}'::jsonbtrue
?textChecks 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
||jsonbConcatenates 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"]
-textDeletes 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[]{}
-integerDeletes 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

FunctionReturn typeDescriptionExampleResult
json_array_length(json) / jsonb_array_length(jsonb)intReturns 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 textReturns 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)anyelementExpands 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 anyelementExpands 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 jsonbExpands 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 textExpands 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)textReturns 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 / jsonbRemoves 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])jsonbReplaces 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])jsonbInserts 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)textReturns 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_aggjsonbAggregates values (including nulls) into a JSON array.See example below.
jsonb_object_aggjsonbAggregates key-value pairs into a JSON object. Keys cannot be null; values can be null.See example below.
is_valid_json(text)booleanReturns 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]             | true

Parsing functions

FunctionDescriptionExampleResult
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.0

The 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_ops and jsonb_holo_path_ops support index lengths of 1 to 127 bytes. Index values longer than 127 bytes are truncated, and truncated fields require a data recheck. Run EXPLAIN ANALYZE to check whether a recheck occurs.
  • jsonb_holo_ops: equivalent to jsonb_ops. Supports ?, ?|, ?&, and @> operators.

  • jsonb_holo_path_ops: equivalent to jsonb_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.