All Products
Search
Document Center

AnalyticDB:Operations on JSON & JSONB data types

Last Updated:Mar 30, 2026

AnalyticDB for PostgreSQL supports JSON and JSONB data types for storing and querying semi-structured data. JSONB stores data in binary format and supports indexing, making it the recommended choice for most workloads. JSON preserves the exact input text, which is useful when whitespace or key ordering must be maintained.

JSON vs JSONB

Aspect JSON JSONB
Storage format Exact input text Binary
Query performance Slower (re-parses on each access) Faster
Indexing support No Yes (GIN, B-tree)
Availability All versions V6.0

Use JSONB for most workloads—faster reads, index support, and efficient containment queries. Use JSON only when you need to preserve the original input format exactly.

Limitations

  • JSON and JSONB columns cannot be used as partition keys.

  • JSON aggregate functions are not supported.

JSON input syntax

A JSON value must be one of: object, array, number, string, or the literal names true, false, or null (all lowercase). For the full specification, see RFC 7159.

-- Scalar value: a number, a quoted string, or a literal name
SELECT '5'::json;

-- Array: zero or more elements of any type
SELECT '[1, 2, "foo", null]'::json;

-- Object: key-value pairs where keys must be quoted strings
SELECT '{"bar": "baz", "balance": 7.77, "active": false}'::json;

-- Nested structures
SELECT '{"foo": [true, "bar"], "tags": {"a": 1, "b": null}}'::json;

All JSON values can be cast to JSONB:

SELECT '5'::jsonb;

Operators

Operators for JSON and JSONB

Operator Right operand type Description Example Result
-> int Gets a JSON array element by zero-based index '[{"a":"foo"},{"b":"bar"},{"c":"baz"}]'::json->2 {"c":"baz"}
-> text Gets a JSON object field by key '{"a":{"b":"foo"}}'::json->'a' {"b":"foo"}
->> int Gets a JSON array element as text '[1,2,3]'::json->>2 3
->> text Gets a JSON object field as text '{"a":1,"b":2}'::json->>'b' 2
#> text[] Gets a JSON object at the specified path '{"a":{"b":{"c":"foo"}}}'::json#>'{a,b}' {"c":"foo"}
#>> text[] Gets a JSON object at the specified path as text '{"a":[1,2,3],"b":[4,5,6]}'::json#>>'{a,2}' 3

JSONB-only operators

Operator Right operand type Description Example
= jsonb Checks equality of two JSONB values '[1,2]'::jsonb = '[1,2]'::jsonb
@> jsonb Checks whether the left value contains the right value '{"a":1,"b":2}'::jsonb @> '{"b":2}'::jsonb
<@ jsonb Checks whether the left value is contained in the right value '{"b":2}'::jsonb <@ '{"a":1,"b":2}'::jsonb
? text Checks whether the string exists as a key or as a string within the JSON value '{"a":1,"b":2}'::jsonb ? 'b'
?| text[] Checks whether any of the strings exist as keys or as strings within the JSON value '{"a":1,"b":2,"c":3}'::jsonb ?| array['b','c']
?& text[] Checks whether all of the strings exist as keys or as strings within the JSON value '["a","b"]'::jsonb ?& array['a','b']

Functions

JSON creation functions

Function Description Example Result
to_json(anyelement) Converts a value to JSON. Arrays and composite types are converted recursively. Scalar values that are not numbers, booleans, or null are returned as quoted strings. to_json('Fred said "Hi."'::text) "Fred said \"Hi.\""
array_to_json(anyarray [, pretty_bool]) Converts an array to a JSON array. If pretty_bool is true, adds line feeds between top-level elements. array_to_json('{{1,5},{99,100}}'::int[]) [[1,5],[99,100]]
row_to_json(record [, pretty_bool]) Converts a row to a JSON object. If pretty_bool is true, adds line feeds between top-level elements. row_to_json(row(1,'foo')) {"f1":1,"f2":"foo"}

JSON processing functions

Function Return type Description Example
json_each(json) set of key text, value json / set of key text, value jsonb Expands the outermost object into key-value pairs select * from json_each('{"a":"foo","b":"bar"}')
json_each_text(json) set of (key text, value text) Expands the outermost object into key-value pairs; values are returned as text select * from json_each_text('{"a":"foo","b":"bar"}')
json_extract_path(from_json json, VARIADIC path_elems text[]) json Returns the JSON value at the given path. Equivalent to the #> operator. json_extract_path('{"f2":{"f3":1},"f4":{"f5":99,"f6":"foo"}}','f4')
json_extract_path_text(from_json json, VARIADIC path_elems text[]) text Returns the JSON value at the given path as text. Equivalent to the #>> operator. json_extract_path_text('{"f2":{"f3":1},"f4":{"f5":99,"f6":"foo"}}','f4','f6')
json_object_keys(json) setof text Returns the set of keys in the outermost object json_object_keys('{"f1":"abc","f2":{"f3":"a","f4":"b"}}')
json_populate_record(base anyelement, from_json json) anyelement Expands the JSON object into a row with columns matching the type of base select * from json_populate_record(null::myrowtype, '{"a":1,"b":2}')
json_populate_recordset(base anyelement, from_json json) set of anyelement Expands the outermost JSON array of objects into a set of rows matching the type of base select * from json_populate_recordset(null::myrowtype, '[{"a":1,"b":2},{"a":3,"b":4}]')
json_array_elements(json) set of json Expands a JSON array into a set of JSON values select * from json_array_elements('[1,true,[2,false]]')

Create a JSONB index

JSONB columns support GIN and B-tree indexes. GIN indexes are most effective for containment queries using the @> operator.

Two GIN operator classes are available:

-- Default operator class (jsonb_ops): indexes each key and value separately
CREATE INDEX idx_name ON table_name USING gin (idx_col);

-- Path-based operator class (jsonb_path_ops): indexes values only
CREATE INDEX idx_name ON table_name USING gin (idx_col jsonb_path_ops);

Choosing between operator classes:

jsonb_ops (default) jsonb_path_ops
Index entries One per key and one per value One per value
Best for Queries filtering by key existence (?) or containment Containment-only queries (@>)

Use jsonb_ops when you need ?, ?\|, or ?& operators in addition to @>. Use jsonb_path_ops when all your queries use @> only.

Examples

Store and query JSON data

-- Create a table with JSON and array columns
CREATE TABLE tj (id serial, ary int[], obj json, num integer);

-- Insert rows
INSERT INTO tj (ary, obj, num) VALUES ('{1,5}'::int[], '{"obj":1}', 5);
INSERT INTO tj (ary, obj, num) VALUES ('{2,5}'::int[], '{"obj":2}', 5);

-- Convert rows to JSON objects
SELECT row_to_json(q) FROM (SELECT id, ary, obj, num FROM tj) AS q;
--                  row_to_json
-- -------------------------------------------
--  {"f1":1,"f2":[1,5],"f3":{"obj":1},"f4":5}
--  {"f1":2,"f2":[2,5],"f3":{"obj":2},"f4":5}

Join tables on a JSON field

Use ->> to extract a JSON field as text for join conditions:

CREATE TABLE tj2 (id serial, ary int[], obj json, num integer);
INSERT INTO tj2 (ary, obj, num) VALUES ('{2,5}'::int[], '{"obj":2}', 5);

-- Join using the ->> operator
SELECT * FROM tj, tj2 WHERE tj.obj->>'obj' = tj2.obj->>'obj';

-- Equivalent join using json_object_field_text()
SELECT * FROM tj, tj2
  WHERE json_object_field_text(tj.obj, 'obj') = json_object_field_text(tj2.obj, 'obj');

Create a function index on a JSON column

Function indexes let you index the result of a JSON path expression, enabling efficient equality lookups on nested fields:

CREATE TEMP TABLE test_json (json_type text, obj json);
INSERT INTO test_json VALUES ('aa', '{"f2":{"f3":1},"f4":{"f5":99,"f6":"foo"}}');
INSERT INTO test_json VALUES ('cc', '{"f7":{"f3":1},"f8":{"f5":99,"f6":"foo"}}');

-- Create an index on the extracted path value
CREATE INDEX i ON test_json (json_extract_path_text(obj, '{f4}'));

-- The planner can use the index for equality queries
SELECT * FROM test_json WHERE json_extract_path_text(obj, '{f4}') = '{"f5":99,"f6":"foo"}';
--  json_type |                 obj
-- -----------+-------------------------------------------
--  aa        | {"f2":{"f3":1},"f4":{"f5":99,"f6":"foo"}}

Compare GIN index performance on JSONB columns

The following example uses 10 million rows to show the impact of GIN indexes on containment queries.

-- Create test tables
CREATE TABLE jtest1 (id int, jdoc json);
CREATE TABLE jtest2 (id int, jdoc jsonb);
CREATE TABLE jtest3 (id int, jdoc jsonb);

-- Generate a random string helper function
CREATE OR REPLACE FUNCTION random_string(INTEGER)
RETURNS TEXT AS
$BODY$
SELECT array_to_string(
    ARRAY (
        SELECT substring(
            '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
            FROM (ceil(random()*62))::int FOR 1
        )
        FROM generate_series(1, $1)
    ),
    ''
)
$BODY$
LANGUAGE sql VOLATILE;

-- Populate jtest1 with 10 million rows
INSERT INTO jtest1
SELECT t.seq,
       ('{"a":{"a1":"a1a1","a2":"a2a2"},"name":"' || random_string(10) || '","b":"bbbbb"}')::json
FROM generate_series(1, 10000000) AS t(seq);

-- Copy to JSONB tables
INSERT INTO jtest2 SELECT id, jdoc::jsonb FROM jtest1;
INSERT INTO jtest3 SELECT id, jdoc::jsonb FROM jtest1;

-- Create GIN indexes
CREATE INDEX idx_jtest2 ON jtest2 USING gin(jdoc);                        -- jsonb_ops
CREATE INDEX idx_jtest3 ON jtest3 USING gin(jdoc jsonb_path_ops);         -- jsonb_path_ops

Query performance comparison:

-- Sequential scan on the unindexed JSON table: ~1,778 ms
EXPLAIN ANALYZE SELECT * FROM jtest1 WHERE jdoc @> '{"name":"N9WP5txmVu"}';

-- GIN index with jsonb_ops: ~1.3 ms
EXPLAIN ANALYZE SELECT * FROM jtest2 WHERE jdoc @> '{"name":"N9WP5txmVu"}';

-- GIN index with jsonb_path_ops: ~1.3 ms
EXPLAIN ANALYZE SELECT * FROM jtest3 WHERE jdoc @> '{"name":"N9WP5txmVu"}';

Both GIN index types reduce query time from ~1,778 ms to ~1.3 ms on 10 million rows. Choose jsonb_path_ops when queries use only @>; use jsonb_ops when you also need key-existence operators (?, ?\|, ?&).

Query JSONB data from Python

Use psycopg2 to connect and run JSON queries:

#!/usr/bin/env python
import json
import psycopg2

def run_query(sql):
    conn = psycopg2.connect("dbname=<your-database>")
    conn.autocommit = True
    cur = conn.cursor()
    cur.execute(sql)
    return cur.fetchall()

rows = run_query("SELECT obj FROM tj;")
for row in rows:
    print(json.loads(row[0]))

Replace <your-database> with your database name.