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.