AnalyticDB for PostgreSQL では、半構造化データの格納およびクエリ実行に JSON および JSONB データ型をサポートしています。JSONB はバイナリ形式でデータを格納し、インデックス作成をサポートするため、ほとんどのワークロードにおいて推奨される選択肢です。一方、JSON は入力テキストを正確に保持するため、空白文字やキーの順序を厳密に維持する必要がある場合に有効です。
JSON と JSONB の比較
| 項目 | JSON | JSONB |
|---|---|---|
| 格納形式 | 入力されたテキストをそのまま保持 | バイナリ形式 |
| クエリ性能 | 遅い(各アクセス時に再解析が必要) | 高速 |
| インデックス作成対応 | いいえ | 可(GIN、B-tree) |
| 可用性 | 全バージョン | V6.0 |
ほとんどのワークロードには JSONB を使用してください — 読み取りが高速で、インデックス作成および包含クエリ(containment query)の効率性も優れています。元の入力フォーマットを厳密に保持する必要がある場合のみ、JSON を使用します。
制限事項
JSON および JSONB の列はパーティションキーとして使用できません。
JSON の集計関数はサポートされていません。
JSON 入力構文
JSON 値は、以下のいずれかである必要があります:オブジェクト、配列、数値、文字列、または小文字のリテラル名 true、false、null。仕様の詳細については、「RFC 7159」をご参照ください。
-- スカラ値:数値、引用符で囲まれた文字列、またはリテラル名
SELECT '5'::json;
-- 配列:任意の型の要素を 0 個以上含む
SELECT '[1, 2, "foo", null]'::json;
-- オブジェクト:キーと値のペア(キーは必ず引用符で囲む)
SELECT '{"bar": "baz", "balance": 7.77, "active": false}'::json;
-- ネストされた構造
SELECT '{"foo": [true, "bar"], "tags": {"a": 1, "b": null}}'::json;すべての JSON 値は JSONB へキャスト可能です:
SELECT '5'::jsonb;演算子
JSON および JSONB 共通の演算子
| 演算子 | 右オペランドの型 | 説明 | 例 | 結果 |
|---|---|---|---|---|
-> | int | ゼロベースのインデックスで JSON 配列の要素を取得 | '[{"a":"foo"},{"b":"bar"},{"c":"baz"}]'::json->2 | {"c":"baz"} |
-> | text | キーで JSON オブジェクトのフィールドを取得 | '{"a":{"b":"foo"}}'::json->'a' | {"b":"foo"} |
->> | int | JSON 配列の要素を文字列として取得 | '[1,2,3]'::json->>2 | 3 |
->> | text | JSON オブジェクトのフィールドを文字列として取得 | '{"a":1,"b":2}'::json->>'b' | 2 |
#> | text[] | 指定されたパスにある JSON オブジェクトを取得 | '{"a":{"b":{"c":"foo"}}}'::json#>'{a,b}' | {"c":"foo"} |
#>> | text[] | 指定されたパスにある JSON オブジェクトを文字列として取得 | '{"a":[1,2,3],"b":[4,5,6]}'::json#>>'{a,2}' | 3 |
JSONB 専用の演算子
| 演算子 | 右オペランドの型 | 説明 | 例 |
|---|---|---|---|
= | jsonb | 2 つの JSONB 値が等しいかどうかをチェック | '[1,2]'::jsonb = '[1,2]'::jsonb |
@> | jsonb | 左側の値が右側の値を含むかどうかをチェック | '{"a":1,"b":2}'::jsonb @> '{"b":2}'::jsonb |
<@ | jsonb | 左側の値が右側の値に含まれるかどうかをチェック | '{"b":2}'::jsonb <@ '{"a":1,"b":2}'::jsonb |
? | text | 文字列が JSON 値のキーまたは文字列として存在するかどうかをチェック | '{"a":1,"b":2}'::jsonb ? 'b' |
?| | text[] | 文字列のいずれかが JSON 値のキーまたは文字列として存在するかどうかをチェック | '{"a":1,"b":2,"c":3}'::jsonb ?| array['b','c'] |
?& | text[] | すべての文字列が JSON 値のキーまたは文字列として存在するかどうかをチェック | '["a","b"]'::jsonb ?& array['a','b'] |
関数
JSON 生成関数
| 関数 | 説明 | 例 | 結果 |
|---|---|---|---|
to_json(anyelement) | 値を JSON に変換します。配列および複合型は再帰的に変換されます。数値、ブール値、または null でないスカラ値は、引用符で囲まれた文字列として返されます。 | to_json('Fred said "Hi."'::text) | "Fred said \"Hi.\"" |
array_to_json(anyarray [, pretty_bool]) | 配列を JSON 配列に変換します。pretty_bool が true の場合、最上位要素間に改行を挿入します。 | array_to_json('{{1,5},{99,100}}'::int[]) | [[1,5],[99,100]] |
row_to_json(record [, pretty_bool]) | 行を JSON オブジェクトに変換します。pretty_bool が true の場合、最上位要素間に改行を挿入します。 | row_to_json(row(1,'foo')) | {"f1":1,"f2":"foo"} |
JSON 処理関数
| 関数 | 戻り値の型 | 説明 | 例 |
|---|---|---|---|
json_each(json) | key text、value json のセット / key text、value jsonb のセット | 最上位のオブジェクトをキーと値のペアに展開します | select * from json_each('{"a":"foo","b":"bar"}') |
json_each_text(json) | (key text、value text) のセット | 最上位のオブジェクトをキーと値のペアに展開します。値は文字列として返されます | select * from json_each_text('{"a":"foo","b":"bar"}') |
json_extract_path(from_json json, VARIADIC path_elems text[]) | json | 指定されたパスにある JSON 値を返します。#> 演算子と同等です。 | json_extract_path('{"f2":{"f3":1},"f4":{"f5":99,"f6":"foo"}}','f4') |
json_extract_path_text(from_json json, VARIADIC path_elems text[]) | text | 指定されたパスにある JSON 値を文字列として返します。#>> 演算子と同等です。 | json_extract_path_text('{"f2":{"f3":1},"f4":{"f5":99,"f6":"foo"}}','f4','f6') |
json_object_keys(json) | text のセット | 最上位のオブジェクト内のキーのセットを返します | json_object_keys('{"f1":"abc","f2":{"f3":"a","f4":"b"}}') |
json_populate_record(base anyelement, from_json json) | anyelement | JSON オブジェクトを、base | select * from json_populate_record(null::myrowtype, '{"a":1,"b":2}') |
json_populate_recordset(base anyelement, from_json json) | anyelement のセット | 最上位の JSON 配列(オブジェクトの配列)を、base | select * from json_populate_recordset(null::myrowtype, '[{"a":1,"b":2},{"a":3,"b":4}]') |
json_array_elements(json) | json のセット | JSON 配列を JSON 値のセットに展開します | select * from json_array_elements('[1,true,[2,false]]') |
JSONB インデックスの作成
JSONB 列では GIN インデックスおよび B-tree インデックスがサポートされています。GIN インデックスは、@> 演算子を使用した包含クエリに対して最も効果的です。
利用可能な GIN オペレータークラスは 2 種類あります:
-- デフォルトのオペレータークラス (jsonb_ops):各キーおよび値を個別にインデックス化
CREATE INDEX idx_name ON table_name USING gin (idx_col);
-- パスベースのオペレータークラス (jsonb_path_ops):値のみをインデックス化
CREATE INDEX idx_name ON table_name USING gin (idx_col jsonb_path_ops);オペレータークラスの選択方法:
jsonb_ops(デフォルト) | jsonb_path_ops | |
|---|---|---|
| 索引項目 | キーごとに 1 件、値ごとに 1 件 | 値ごとに 1 件 |
| 推奨用途 | キーの存在をフィルターするクエリ(?)および包含クエリ | 包含クエリのみ(@>) |
jsonb_ops を使用するのは、?、?\|、?& 演算子に加えて @> も使用する場合です。jsonb_path_ops を使用するのは、すべてのクエリで @> のみを使用する場合です。
サンプル
JSON データの格納およびクエリ実行
-- JSON および配列の列を含むテーブルを作成
CREATE TABLE tj (id serial, ary int[], obj json, num integer);
-- 行を挿入
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);
-- 行を JSON オブジェクトに変換
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}JSON フィールドによるテーブル結合
結合条件で JSON フィールドを文字列として抽出するには、->> を使用します:
CREATE TABLE tj2 (id serial, ary int[], obj json, num integer);
INSERT INTO tj2 (ary, obj, num) VALUES ('{2,5}'::int[], '{"obj":2}', 5);
-- ->> 演算子を使用した結合
SELECT * FROM tj, tj2 WHERE tj.obj->>'obj' = tj2.obj->>'obj';
-- json_object_field_text() を使用した同等の結合
SELECT * FROM tj, tj2
WHERE json_object_field_text(tj.obj, 'obj') = json_object_field_text(tj2.obj, 'obj');JSON 列に対する関数インデックスの作成
関数インデックスを使用すると、JSON パス式の結果をインデックス化でき、ネストされたフィールドに対する効率的な等価検索が可能になります:
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 INDEX i ON test_json (json_extract_path_text(obj, '{f4}'));
-- 実行計画は等価クエリでこのインデックスを利用できます
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"}}JSONB 列における GIN インデックスのパフォーマンス比較
以下の例では、1,000 万行のデータを使用して、GIN インデックスが包含クエリに与える影響を示します。
-- テストテーブルを作成
CREATE TABLE jtest1 (id int, jdoc json);
CREATE TABLE jtest2 (id int, jdoc jsonb);
CREATE TABLE jtest3 (id int, jdoc jsonb);
-- ランダム文字列ヘルパー関数を作成
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;
-- jtest1 に 1,000 万行を挿入
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);
-- JSONB テーブルにコピー
INSERT INTO jtest2 SELECT id, jdoc::jsonb FROM jtest1;
INSERT INTO jtest3 SELECT id, jdoc::jsonb FROM jtest1;
-- GIN インデックスを作成
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クエリパフォーマンスの比較:
-- インデックスなしの JSON テーブルでの逐次スキャン:約 1,778 ms
EXPLAIN ANALYZE SELECT * FROM jtest1 WHERE jdoc @> '{"name":"N9WP5txmVu"}';
-- jsonb_ops を使用した GIN インデックス:約 1.3 ms
EXPLAIN ANALYZE SELECT * FROM jtest2 WHERE jdoc @> '{"name":"N9WP5txmVu"}';
-- jsonb_path_ops を使用した GIN インデックス:約 1.3 ms
EXPLAIN ANALYZE SELECT * FROM jtest3 WHERE jdoc @> '{"name":"N9WP5txmVu"}';どちらの GIN インデックスタイプも、1,000 万行におけるクエリ時間を約 1,778 ms から約 1.3 ms まで短縮します。jsonb_path_ops は @> のみを使用するクエリに、jsonb_ops はキーの存在をチェックする演算子(?、?\|、?&)も併用するクエリにそれぞれ適しています。
Python からの JSONB データのクエリ実行
psycopg2 を使用して接続し、JSON クエリを実行します:
#!/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]))<your-database> をご利用のデータベース名に置き換えてください。