PostgreSQLでJSONを扱うときは jsonb 型を使い、->・->> で取り出し、@> で検索し、jsonb_set・||・- で更新します。 jsonb はJSONを分解したバイナリ形式で保存する型で、検索用の演算子とGINインデックスが使えるのが特長です。

この記事では、取得・検索・更新・削除・展開・集約の演算子と関数を、PostgreSQL 16.13 の実行結果つきで一覧にしました。json と jsonb の違いも実行で確かめ、GINインデックスがどの書き方で効くかを50万行で EXPLAIN ANALYZE して実測しています。PG17 以降の JSON_TABLE なども公式に沿って紹介します。

PostgreSQL jsonb 操作の早見表

「やりたいこと → 書き方」をこの表で探してください。 data は jsonb 列です。

やりたいこと 書き方 ポイント
値をJSONのまま取り出す data->'customer' 結果は jsonb
値を文字列で取り出す data->>'status' 結果は text。比較・表示はこちら
配列のn番目(0始まり) data->'items'->0、後ろから ->-1
深い階層をパスで data#>'{items,0}' / data#>>'{items,0,sku}' jsonb / text
添字で参照・更新(PG14〜) data['customer']['name'] 結果は jsonb
含む形で検索 data @> '{"status":"pending"}' GINが効く
キー(配列なら要素)があるか data ? 'coupon' jsonb_ops のGINが効く
いずれか/すべてのキー ?| array[...] / ?& array[...]
jsonpath で条件検索 data @? '$.items[*] ? (@.price > 1000)' GINが効く
jsonpath の述語 data @@ '$.customer.rank == "gold"' GINが効く
値を置き換え・キー追加 jsonb_set(data, '{status}', '"shipped"') 親キーがないと追加されない
浅くマージ・配列に追加 data || '{"priority":"high"}' 入れ子は丸ごと置き換え
キー削除 data - 'coupon' / data - '{a,c}'::text[] 配列は - 1 で位置
深い場所を削除 data #- '{a,b,c}'
配列に挿入 jsonb_insert(data, '{tags,1}', '"b"') 第4引数 true で後ろへ
null のキーを除く jsonb_strip_nulls(data) 入れ子も対象
条件に合う要素を抽出 jsonb_path_query(data, '$.items[*] ? (@.qty >= 2).sku') 複数行を返す
配列を行に展開 jsonb_array_elements(_text)(data->'tags')
オブジェクトをキーと値に jsonb_each(_text)(data) / jsonb_object_keys(data)
配列のオブジェクトを表に jsonb_to_recordset(data->'items') AS i(sku text, qty int) 列名と型を指定
行を配列・オブジェクトに集約 jsonb_agg(x ORDER BY ...) / jsonb_object_agg(k, v)
値から組み立て jsonb_build_object / jsonb_build_array / to_jsonb
JSONとして正しいか(PG16〜) v IS JSON [OBJECT|ARRAY] [WITH UNIQUE KEYS] text に使える
標準SQLで組み立て(PG16〜) JSON_OBJECT / JSON_ARRAY / JSON_OBJECTAGG / JSON_ARRAYAGG 既定は json 型
表に変換・値を取得(PG17〜) JSON_TABLE / JSON_VALUE / JSON_QUERY / JSON_EXISTS PG16では使えない

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PostgreSQL jsonb操作一覧|演算子早見表とGINインデックス実測」

検証環境と使うテーブル

検証は PostgreSQL 16.13(Ubuntu 24.04)です。 2026年9月時点の最新メジャーは 18(18.6)、19 はベータ版で、16 は2028年11月までサポートされます(公式 Versioning Policy)。17 以降の機能はその都度明記します。

CREATE TABLE orders (
  id    bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  data  jsonb NOT NULL
);
INSERT INTO orders (data) VALUES
('{"customer":{"name":"佐藤","rank":"gold"},"status":"shipped","items":[{"sku":"A-1","qty":2,"price":1200},{"sku":"B-7","qty":1,"price":800}],"tags":["gift","express"],"note":null}'),
('{"customer":{"name":"鈴木","rank":"silver"},"status":"pending","items":[{"sku":"A-1","qty":1,"price":1200}],"tags":["express"]}'),
('{"customer":{"name":"高橋","rank":"gold"},"status":"pending","items":[{"sku":"C-3","qty":5,"price":300}],"tags":[],"coupon":"WELCOME"}');

psql は \pset null '(null)' を設定しています。SQL中の -- → は PG16.13 で実際に返った結果です。

json と jsonb の違いを実行で確認

json は入力文字列をそのまま保存し、jsonb は分解・正規化して保存します。

SELECT '{"b":1, "a":2, "a":3}'::json  AS json_val,
       '{"b":1, "a":2, "a":3}'::jsonb AS jsonb_val;
SELECT '{"x": [1, 2,   3]}'::jsonb;                     -- → {"x": [1, 2, 3]}
SELECT '{"a":1,"b":2}'::jsonb = '{"b":2, "a":1}'::jsonb; -- → t
SELECT '{"a":1}'::json = '{"a":1}'::json;
       json_val        |    jsonb_val
-----------------------+------------------
 {"b":1, "a":2, "a":3} | {"a": 3, "b": 1}

ERROR:  operator does not exist: json = json
観点 json jsonb
保存形式 入力テキストそのまま 分解したバイナリ
キーの順序 入力順 保持しない(短いキー順→辞書順)
重複キー すべて残る 最後の値だけ残る
空白 そのまま 除去
= 比較 できない(エラー) できる(順序違いも等しい)
@> ? @? とGIN なし あり

迷ったら jsonb で大丈夫です(公式ドキュメントも jsonb を推奨)。json は原文を一字一句残したいときに使います。

値を取り出す(-> ->> #> #>> と添字)

-> はJSONのまま、->> は text で返します。 中をたどるときは ->、最後の比較・表示は ->> です。

SELECT id,
       data->'customer'->>'name'    AS name,
       pg_typeof(data->'status')    AS t1,
       pg_typeof(data->>'status')   AS t2,
       data#>>'{items,0,sku}'       AS first_sku,
       data->'items'->-1->>'sku'    AS last_sku,
       data['customer']['rank']     AS rank_sub
FROM orders ORDER BY id;
 id | name |  t1   |  t2  | first_sku | last_sku | rank_sub
----+------+-------+------+-----------+----------+----------
  1 | 佐藤 | jsonb | text | A-1       | B-7      | "gold"
  2 | 鈴木 | jsonb | text | A-1       | A-1      | "silver"
  3 | 高橋 | jsonb | text | C-3       | C-3      | "gold"

JSONの null は -> だと jsonb の null、->> だと SQL の NULL。ないキーはどちらも SQL の NULL です。

SELECT data->>'note' AS note, data->'note' AS note_json, data->>'nokey' AS nokey
FROM orders WHERE id = 1;
  note  | note_json | nokey
--------+-----------+--------
 (null) | null      | (null)

「キーがない」と「値が null」を区別したいときは data ? 'note' を併用します。

取り出しでよくあるエラーと直し方

書いたSQL PG16の結果 直し方
data->'status' = 'shipped' ERROR: invalid input syntax for type json DETAIL: Token "shipped" is invalid. data->>'status' = 'shipped' か data->'status' = '"shipped"'
data->'items'->>'qty'(items は配列) エラーにならず NULL data->'items'->0->>'qty' か展開関数
(data->'qty')::int(値が null) ERROR: cannot cast jsonb null to type integer (data->>'qty')::int(NULLになる)。PG18 からは NULL を返す仕様
(data->'qty')::int(値が "5") ERROR: cannot cast jsonb string to type integer (data->>'qty')::int

計算に使うときは (data->>'qty')::int の形に揃えると安全です。

検索する(@> <@ ? ?| ?&)

「この形を含む行」を探す @>(包含)が jsonb 検索の基本です。 入れ子や配列の要素にも使え、GINインデックスが効きます。

SELECT id FROM orders WHERE data @> '{"status":"pending"}';            -- → 2, 3
SELECT id FROM orders WHERE data @> '{"customer":{"rank":"gold"}}';    -- → 1, 3
SELECT id FROM orders WHERE data @> '{"items":[{"sku":"A-1"}]}';       -- → 1, 2
SELECT '{"a":1}'::jsonb <@ '{"a":1,"b":2}'::jsonb;                     -- → t
SELECT id FROM orders WHERE data ? 'coupon';                            -- → 3
SELECT id FROM orders WHERE data->'tags' ? 'gift';                      -- → 1
SELECT id FROM orders WHERE data->'tags' ?| array['gift','express'];    -- → 1, 2
SELECT id FROM orders WHERE data->'tags' ?& array['gift','express'];    -- → 1

配列の @> は、items のどれか1件が sku: A-1 を持てばヒットします。

? をプレースホルダーに使うドライバー(JDBCなど)では ?? と書くか jsonb_exists(data, 'coupon') を使います(関数形式はGINが効きません)。

jsonpath で条件検索・抽出する

「配列の中に price が1000を超える要素がある」のような条件は jsonpath で書きます。 $ がルート、[*] が全要素、? (...) がフィルター、@ が今の要素です。

SELECT id, jsonb_path_query(data, '$.items[*] ? (@.qty >= 2).sku') AS sku
FROM orders ORDER BY id;                                                  -- → 1 "A-1" / 3 "C-3"
SELECT id FROM orders WHERE data @? '$.items[*] ? (@.price > 1000)';      -- → 1, 2
SELECT id FROM orders WHERE jsonb_path_exists(data, '$.items[*] ? (@.price > 1000)'); -- → 1, 2
SELECT id FROM orders WHERE data @@ '$.customer.rank == "gold"';          -- → 1, 3
SELECT jsonb_path_query('{"a":[1,2,3,4]}', '$.a[*] ? (@ > $min)', '{"min":2}'); -- → 3, 4(2行)
SELECT id, jsonb_path_query_array(data, '$.items[*].sku')   AS skus,
           jsonb_path_query_first(data, '$.items[*].price') AS first_price
FROM orders ORDER BY id;
 id |      skus      | first_price
----+----------------+-------------
  1 | ["A-1", "B-7"] | 1200
  2 | ["A-1"]        | 1200
  3 | ["C-3"]        | 300

更新・削除する(jsonb_set / || / - / #- / jsonb_insert)

jsonb は「新しい値を作って列を置き換える」形で更新します。 まず関数単体の動きです。

SELECT jsonb_set('{"a":{"b":1}}', '{a,b}', '99');            -- → {"a": {"b": 99}}
SELECT jsonb_set('{"a":{}}', '{a,new}', '"v"');               -- → {"a": {"new": "v"}}
SELECT jsonb_set('{"a":{}}', '{x,y}', '1');                   -- → {"a": {}}(親 x がないので何もしない)
SELECT '{"a":1}'::jsonb || '{"b":2}';                         -- → {"a": 1, "b": 2}
SELECT '{"a":{"x":1}}'::jsonb || '{"a":{"y":2}}';             -- → {"a": {"y": 2}}(浅いマージ)
SELECT '{"a":1,"b":2,"c":3}'::jsonb - 'b';                    -- → {"a": 1, "c": 3}
SELECT '["x","y","z"]'::jsonb - 1;                            -- → ["x", "z"]
SELECT '{"a":{"b":{"c":1,"d":2}}}'::jsonb #- '{a,b,c}';       -- → {"a": {"b": {"d": 2}}}
SELECT jsonb_insert('{"tags":["a","c"]}', '{tags,1}', '"b"');        -- → {"tags": ["a", "b", "c"]}
SELECT jsonb_insert('{"tags":["a","c"]}', '{tags,-1}', '"z"', true); -- → {"tags": ["a", "c", "z"]}
SELECT jsonb_strip_nulls('{"a":1,"b":null,"c":{"d":null}}');         -- → {"a": 1, "c": {}}

ポイントは3つです。

  • jsonb_set は途中の親キーがないと何もしません。 親ごと作るなら || で先に足します。
  • || は浅いマージです。 入れ子の1キーだけ変えるなら jsonb_set を使います。
  • 新しい値は jsonb で渡します。 文字列は '"shipped"' のようにダブルクォートを含めるか、to_jsonb('shipped'::text) を使います。

UPDATE のコピペ集

BEGIN;
UPDATE orders SET data = jsonb_set(data, '{status}', '"shipped"') WHERE id = 2
RETURNING data->>'status';                        -- → shipped
UPDATE orders SET data = jsonb_set(data, '{customer,rank}', '"platinum"') WHERE id = 1
RETURNING data->'customer';                       -- → {"name": "佐藤", "rank": "platinum"}
UPDATE orders SET data = data || '{"priority":"high"}' WHERE id = 3
RETURNING data->'priority';                       -- → "high"
UPDATE orders SET data = data - 'coupon' WHERE id = 3
RETURNING data ? 'coupon';                        -- → f
UPDATE orders SET data = jsonb_set(data, '{tags}', (data->'tags') || '"vip"') WHERE id = 3
RETURNING data->'tags';                           -- → ["vip"]
UPDATE orders SET data['customer']['rank'] = '"bronze"' WHERE id = 2
RETURNING data['customer'];                       -- → {"name": "鈴木", "rank": "bronze"}(PG14〜)
ROLLBACK;

jsonb_set に SQL の NULL を渡すと列全体が NULL になる

jsonb_set は引数に SQL の NULL があると結果全体が NULL になります。 SET data = jsonb_set(data, '{a}', $1) の $1 が NULL だと、列の中身がまるごと NULL になります。

SELECT jsonb_set('{"a":1}', '{a}', NULL);                                   -- → (null)
SELECT jsonb_set_lax('{"a":1,"b":2}', '{a}', NULL);                         -- → {"a": null, "b": 2}
SELECT jsonb_set_lax('{"a":1,"b":2}', '{a}', NULL, true, 'delete_key');     -- → {"b": 2}

NULL になりうる値は jsonb_set_lax(PG13〜)で更新し、列には NOT NULL を付けておくと安心です。

展開する(配列を行に・JSONを表に)

配列を行に分けて JOIN・集計したいときは、展開関数を FROM 句に置きます。

SELECT o.id, i.sku, i.qty, i.price, i.qty * i.price AS subtotal
FROM orders o, jsonb_to_recordset(o.data->'items') AS i(sku text, qty int, price int)
ORDER BY o.id, i.sku;
 id | sku | qty | price | subtotal
----+-----+-----+-------+----------
  1 | A-1 |   2 |  1200 |     2400
  1 | B-7 |   1 |   800 |      800
  2 | A-1 |   1 |  1200 |     1200
  3 | C-3 |   5 |   300 |     1500

カンマ結合だと、配列が空の行は結果から消えます。 残したいときは LEFT JOIN LATERAL ... ON true、順番は WITH ORDINALITY で取れます。

SELECT o.id, t.tag, t.ord
FROM orders o
LEFT JOIN LATERAL jsonb_array_elements_text(o.data->'tags') WITH ORDINALITY AS t(tag, ord) ON true
ORDER BY o.id, t.ord;
 id |   tag   |  ord
----+---------+--------
  1 | gift    |      1
  1 | express |      2
  2 | express |      1
  3 | (null)  | (null)

カンマ結合で書くと id=3("tags":[])の行は出ません。jsonb_each(data->'customer') はキーと値の2列(name / "佐藤" など)を返します。

集約・組み立てる(jsonb_agg / jsonb_object_agg / jsonb_build_object)

行をJSONにまとめて返すときは、集約関数と組み立て関数を組み合わせます。

SELECT jsonb_agg(data->'customer'->>'name' ORDER BY id) FROM orders;
-- → ["佐藤", "鈴木", "高橋"]
SELECT jsonb_object_agg(id, data->>'status') FROM orders;
-- → {"1": "shipped", "2": "pending", "3": "pending"}
SELECT jsonb_build_object('id', id, 'name', data#>>'{customer,name}',
                          'items', jsonb_array_length(data->'items'))
FROM orders WHERE id = 1;
-- → {"id": 1, "name": "佐藤", "items": 2}

PostgreSQL 16 の SQL/JSON(IS JSON・JSON_OBJECT など)

PostgreSQL 16 で、標準SQLの IS JSON 述語と JSON_OBJECT などの組み立て関数が使えるようになりました。

SELECT v, v IS JSON AS is_json, v IS JSON OBJECT AS is_obj,
          v IS JSON ARRAY AS is_arr, v IS JSON WITH UNIQUE KEYS AS uniq
FROM (VALUES ('{"a":1}'), ('[1,2]'), ('{"a":1,"a":2}'), ('abc')) AS t(v);
       v       | is_json | is_obj | is_arr | uniq
---------------+---------+--------+--------+------
 {"a":1}       | t       | t      | f      | t
 [1,2]         | t       | f      | t      | t
 {"a":1,"a":2} | t       | t      | f      | f
 abc           | f       | f      | f      | f
SELECT JSON_OBJECT('id': 1, 'name': '佐藤');        -- → {"id" : 1, "name" : "佐藤"}
SELECT JSON_OBJECT('a': NULL);                      -- → {"a" : null}
SELECT JSON_ARRAY(1, 'x', NULL);                    -- → [1, "x"]
SELECT JSON_ARRAY(1, 'x', NULL NULL ON NULL);       -- → [1, "x", null]
SELECT JSON_OBJECT('id': 1 RETURNING jsonb);        -- → {"id": 1}

既定では json 型で返るので、jsonb が欲しいときは RETURNING jsonb を付けます。 JSON_ARRAY は既定で NULL を省き、JSON_OBJECT は既定で null を入れる点も違います。IS JSON は CHECK (body IS JSON OBJECT) のように制約にも使えます。

PostgreSQL 17 以降の JSON_TABLE・JSON_VALUE(公式準拠)

JSON_TABLE と JSON_VALUE / JSON_QUERY / JSON_EXISTS は PostgreSQL 17 で追加されました。 PG16 では次のエラーになります。

SELECT * FROM JSON_TABLE('[{"a":1}]', '$[*]' COLUMNS (a int PATH '$.a'));
ERROR:  syntax error at or near "COLUMNS"
SELECT JSON_VALUE('{"a":1}', '$.a');
ERROR:  function json_value(unknown, unknown) does not exist

以下は公式ドキュメントに沿って書いた PG17 以降向けの例で、この記事の環境(PG16)では未実行です。

-- PostgreSQL 17 以降(未実行)
SELECT o.id, jt.*
FROM orders o,
     JSON_TABLE(o.data, '$.items[*]'
       COLUMNS (
         rn    FOR ORDINALITY,
         sku   text    PATH '$.sku',
         qty   int     PATH '$.qty',
         price numeric PATH '$.price' DEFAULT 0 ON EMPTY
       )) AS jt;
版 JSON関連の主な追加(公式リリースノート)
14 jsonb の添字 data['key']
16 IS JSON、JSON_OBJECT JSON_ARRAY と集約版
17 JSON_TABLE、JSON_EXISTS JSON_QUERY JSON_VALUE、JSON() など、jsonpath の .integer() .date() など
18 jsonb の null をスカラー型へキャストすると NULL、jsonb_strip_nulls に配列の null を除く引数

GINインデックスが効く書き方・効かない書き方(50万行で実測)

jsonb 列全体の GIN インデックスは @> ? @? @@ に効き、data->>'key' = '値' には効きません。 50万行(テーブル 75MB)で確かめました。

CREATE TABLE events (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, data jsonb NOT NULL);
INSERT INTO events (data)
SELECT jsonb_build_object(
  'user_id', g % 5000,
  'type',   (ARRAY['view','click','cart','order'])[1 + g % 4],
  'device', CASE WHEN g % 3 = 0 THEN 'ios' WHEN g % 3 = 1 THEN 'android' ELSE 'web' END,
  'meta',   jsonb_build_object('campaign', 'c' || (g % 200), 'ab', g % 2 = 0))
FROM generate_series(1, 500000) AS g;
UPDATE events SET data = data || '{"coupon":"SPRING"}' WHERE id % 1000 = 0;  -- 500行だけ
ANALYZE events;
CREATE INDEX events_data_gin ON events USING gin (data);   -- 既定の jsonb_ops
EXPLAIN ANALYZE SELECT count(*) FROM events WHERE data @> '{"meta":{"campaign":"c42"}}';
 Aggregate  (cost=8350.73..8350.74 rows=1 width=8) (actual time=16.922..16.923 rows=1 loops=1)
   ->  Bitmap Heap Scan on events  (cost=60.79..8338.13 rows=5040 width=0) (actual time=9.449..16.781 rows=2500 loops=1)
         Recheck Cond: (data @> '{"meta": {"campaign": "c42"}}'::jsonb)
         ->  Bitmap Index Scan on events_data_gin  (cost=0.00..59.53 rows=5040 width=0) (actual time=8.501..8.501 rows=2500 loops=1)
 Execution Time: 16.948 ms

実測まとめ(PG16.13・50万行・各1回の計測なので目安です):

条件の書き方 索引なし GIN jsonb_ops GIN jsonb_path_ops 式インデックス
data @> '{"meta":{"campaign":"c42"}}' 143.6 ms(Seq) 16.9 ms 4.1 ms ―
data @? '$.meta ? (@.campaign == "c42")' ― 8.8 ms ― ―
data ? 'coupon'(500行が該当) ― 0.28 ms 49.4 ms(Seq、非対応) ―
data->'meta'->>'campaign' = 'c42' ― 96.1 ms(Seq) ― 2.97 ms
data#>>'{meta,campaign}' = 'c42' ― ― ― 98.8 ms(Seq、式が違う)

ほぼ全行が該当する data ? 'device' では、GINがあっても Seq Scan が選ばれました(120 ms)。

jsonb_ops と jsonb_path_ops の選び方

観点 jsonb_ops(既定) jsonb_path_ops
作り方 USING gin (data) USING gin (data jsonb_path_ops)
対応演算子 @> ? ?| ?& @? @@ @> @? @@(? 系は非対応)
サイズ(今回) 9,656 kB 6,432 kB
@> の速さ(今回) 16.9 ms 4.1 ms

@> 中心なら jsonb_path_ops、? も使うなら jsonb_ops が目安です。

特定キーの =・範囲検索には式インデックス

よく使うキーが決まっているなら、そのキーの B-tree 式インデックスが小さく速く、範囲検索や ORDER BY にも使えます。

CREATE INDEX events_campaign_idx ON events ((data->'meta'->>'campaign'));
CREATE INDEX events_user_idx     ON events (((data->>'user_id')::int));   -- 数値で比較
EXPLAIN (ANALYZE, COSTS OFF) SELECT count(*) FROM events WHERE (data->>'user_id')::int = 123;
 Aggregate (actual time=0.874..0.875 rows=1 loops=1)
   ->  Bitmap Heap Scan on events (actual time=0.068..0.861 rows=100 loops=1)
         Recheck Cond: (((data ->> 'user_id'::text))::integer = 123)
         ->  Bitmap Index Scan on events_user_idx (actual time=0.052..0.053 rows=100 loops=1)
 Execution Time: 0.896 ms

式インデックスは、WHERE の式が定義と同じ形のときだけ使われます。 上の表のとおり、-> ->> で作ったインデックスは、同じ意味の #>> の条件には使われませんでした。チームで書き方を揃えましょう。

MySQL の JSON 型との対応表

MySQL 8.0 の JSON 型もバイナリ保存で、重複キーは最後の値が残ります(MySQL 8.0.46 で {"b":1,"a":2,"a":3} が {"a": 3, "b": 1} で保存されることを確認)。

やりたいこと PostgreSQL jsonb MySQL 8.0 JSON
取り出す(JSON / 文字列) data->'a' / data->>'a' data->'$.a' / data->>'$.a'
含むか / キーがあるか @> / ? JSON_CONTAINS / JSON_CONTAINS_PATH
値を設定 / キー削除 jsonb_set / data - 'a' JSON_SET / JSON_REMOVE
表に展開 / 集約 jsonb_to_recordset / jsonb_agg JSON_TABLE / JSON_ARRAYAGG
インデックス GIN、式インデックス 生成列+索引、Multi-Valued Index

MySQL の ->> は列にだけ書けるため、JSON_SET(...)->>'$.b' は ERROR 1064 でした。関数の結果には JSON_UNQUOTE(JSON_EXTRACT(...)) を使います。

jsonb にするか、普通の列にするかの判断基準

jsonb は柔軟ですが、型チェックや外部キーは使えません。 次の表で判断してください。

データの性質 おすすめ 理由
どの行にも必ずある・JOINや集計で使う項目 普通の列 NOT NULL・型・CHECK、安定した実行計画
他テーブルを参照する(会員ID・商品ID) 普通の列+外部キー 整合性をDBが守る
項目が行ごとに違う(商品ごとの属性、設定値) jsonb 列を増やさず持てる
外部APIの応答・Webhook本文 jsonb 形が変わっても保存できる
途中から検索が増えた項目 生成列・式インデックス、のちに普通の列へ 段階的に性能を上げられる

途中から検索が増えた項目は、生成列で普通の列として取り出せます(PG16で実行確認済み)。

ALTER TABLE orders ADD COLUMN status text GENERATED ALWAYS AS (data->>'status') STORED;
CREATE INDEX orders_status_idx ON orders (status);

よくある質問

Q. json と jsonb はどちらを使えばよいですか? A. 検索や更新をするなら jsonb です。jsonb は重複キーの最後の値だけを残し、キーの順序も変わる点に注意します。

Q. data->>'status' = 'pending' にインデックスを効かせるには? A. CREATE INDEX ON orders ((data->>'status')); の式インデックスを作ります。列全体のGINは ->> の = には使われません。GINを使うなら data @> '{"status":"pending"}' と書きます。

まとめ

  • 検索・更新するなら jsonb。 json はキーの順序・重複・空白をそのまま残す型です。
  • 取り出しは ->(jsonb)と ->>(text)、比較と計算は ->> とキャストで行います。
  • 検索は @> が基本、複雑な条件は jsonpath の @? @@。 どちらも GIN が効きます。
  • 更新は jsonb_set || - #-。 SQLの NULL で列全体が NULL になるので jsonb_set_lax を活用します。
  • GIN は jsonb_path_ops(@> 中心で小さく速い)と jsonb_ops(? も使う)を使い分け、->> の = には式インデックスを作ります。
  • PG16 で IS JSON・JSON_OBJECT など、PG17 で JSON_TABLE・JSON_VALUE などが使えます。
  • 必ずある項目・JOINする項目は普通の列、行ごとに形が違う項目は jsonb が判断の目安です。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PostgreSQL jsonb操作一覧|演算子早見表とGINインデックス実測」

株式会社RJCでは、データベースの設計や実行計画の確認を、プロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?