مرکزی مواد پر جائیں
友田 陽大
PostgreSQL internals & performance
PostgreSQL
アーキテクチャ設計
TypeScript

PostgreSQL JSONB 実践ガイド(json との違い・演算子・GIN/式インデックス設計・型安全な境界・v18対応)

PostgreSQL の JSONB を本番で正しく使うための実践ガイド。json と jsonb の違い、->/->>/@>/? などの演算子、包含・存在の落とし穴、jsonb_set などの更新、SQL/JSON パス、GIN(jsonb_ops と jsonb_path_ops)と式B-treeの索引設計の判断、正規化列との使い分け、TypeScript/Zod での型安全な境界までを公式ドキュメントに忠実な実コードで解説します。

Published
Reading time
12 min read
Author
友田 陽大
شیئر کریں

「スキーマが固まらない」「設定や属性が可変」——そんなとき、専用の NoSQL を増やす前に、PostgreSQL の JSONB を思い出してください。RDB のトランザクション・結合・制約を保ったまま、半構造データを1つの列に格納し、索引まで張れます

ただし JSONB は「何でも入れていい便利な袋」ではありません。正規化列との使い分け包含・存在演算子の落とし穴、そして索引設計の判断を誤ると、遅くて壊れやすいテーブルになります。この記事は、JSONB を本番品質で使うための判断基準を、PostgreSQL 公式ドキュメントに忠実に整理します。データベース・RLS クラスタとも地続きの、実務テーマです。

この記事のルール:型の挙動・演算子・索引・関数・PostgreSQL 18 の変更点は、すべて PostgreSQL 18 公式ドキュメント(2026年6月時点) に基づきます。JSON 関数は版による差が大きいため(JSON_TABLE は PG17、IS JSON は PG16 等)、版を明記します。


1. json と jsonb:原則 jsonb

PostgreSQL には jsonjsonb の2つの型があります。違いは公式が明快に述べています。

json 型は入力テキストの正確なコピーを保存し、処理関数が実行のたびに再パースする。一方 jsonb分解されたバイナリ形式で保存され、入力時の変換コストでわずかに遅いが、再パースが不要なので処理は大幅に速いjsonb索引もサポートする。

そして「どちらを使うべきか」も明言されています。

一般に、ほとんどのアプリケーションは jsonb で保存すべき。オブジェクトキーの順序に関するレガシーな前提など、特殊な要件がある場合を除く。

結論:迷ったら jsonb ただし jsonb には正規化の副作用があります(公式)。

  • 空白を保持しない(整形は失われる)。
  • キーの順序を保持しない
  • 重複キーを保持しない(同じキーが複数あると最後の値だけが残る)。
  • 数値は内部で numeric 型になる(範囲外を拒否、E 記法は正規化、意味のない末尾の0は保持)。
SELECT '{"b":1, "a":2, "a":3}'::jsonb;
-- → {"a": 3, "b": 1}   キー順は変わり、重複 a は最後の 3 が勝つ

2. アクセス演算子:-> と ->> を間違えない

最頻出の操作。-> は JSON型を返し、->> はテキストを返す——この違いが命です。

演算子戻り値
->json/jsonbdata -> 'user'(オブジェクト)、data -> 0(配列要素)
->>textdata ->> 'name'(文字列として取り出す・比較する)
#>json/jsonbdata #> '{user,address,city}'(パス)
#>>textdata #>> '{user,address,city}'(パスでテキスト)
data['key']jsonb添字アクセス(PG14+、jsonb のみ。代入も可)
-- 比較・WHERE には ->>(テキスト)。-> はJSON型なので文字列比較に使えない
SELECT * FROM users WHERE profile ->> 'plan' = 'pro';

-- ネストはパスで一発
SELECT profile #>> '{address,city}' AS city FROM users;

-- 添字アクセス(PG14+)。代入も可能
UPDATE users SET profile['plan'] = '"enterprise"'::jsonb WHERE id = 1;

抽出演算子は、キーや要素が存在しなければ NULL を返す(エラーにならない)。これは安全な一方、「キーが無い」と「値が NULL」の区別が必要なら ?(存在演算子)で確認します。


3. 包含 @> と存在 ?:ここに落とし穴がある

JSONB 検索の主役は包含 @>存在 ? ですが、両方とも直感に反する挙動があります。

包含 @>:順序は無視、でもネストは平坦化しない

公式の原則:「包含される側が、包含する側に構造とデータ内容として一致しなければならない(一部の非一致要素は捨ててよい)。配列要素の順序は無視され、重複要素は1回として扱う」。

-- オブジェクトの部分一致:tags に "pg" を含み plan が "pro" の行
SELECT * FROM docs WHERE data @> '{"plan":"pro"}';
SELECT * FROM docs WHERE data @> '{"tags":["pg"]}';   -- 配列の順序は無視される

落とし穴@>ネストを平坦化しません。公式の例:

SELECT '[1, 2, [1, 3]]'::jsonb @> '[1, 3]'::jsonb;    -- false!(ネストの中の [1,3] とは別物)
SELECT '[1, 2, [1, 3]]'::jsonb @> '[[1, 3]]'::jsonb;  -- true(同じネスト階層で問う)
SELECT '{"foo":{"bar":"baz"}}'::jsonb @> '{"bar":"baz"}'::jsonb;  -- false(階層が違う)

つまり問う側も、相手と同じ階層構造で書く必要があります。

存在 ?:トップレベルのキーしか見ない

公式:「? は、文字列が jsonb 値のトップレベルにキーまたは配列要素として現れるかを問う。ネストしたキーは見ない」。

SELECT * FROM docs WHERE data ? 'email';       -- トップレベルに email キーがあるか
SELECT * FROM docs WHERE data ?| array['a','b'];  -- a か b のどちらかが存在
SELECT * FROM docs WHERE data ?& array['a','b'];  -- a と b の両方が存在
-- data ? 'nested_key' は、ネスト内の nested_key を見つけられない(トップレベルのみ)

ネスト内のキー存在を問いたいなら、#> でパスを掘ってから ? を使うか、後述の SQL/JSON パス@?)を使います。


4. 更新:jsonb_set / || / - / #-

JSONB の部分更新は、専用関数で行います(UPDATE 自体は行全体の書き換え=MVCC 上は新版を作る点に注意)。

-- 値の差し替え/追加(既定で path が無ければ作る)
UPDATE users SET profile = jsonb_set(profile, '{plan}', '"pro"') WHERE id = 1;

-- マージ(|| は浅いマージ。重複キーは右側が勝つ。再帰はしない)
UPDATE users SET profile = profile || '{"verified": true}' WHERE id = 1;

-- キー削除(- はキー/要素、#- はパス)
UPDATE users SET profile = profile - 'temp_token' WHERE id = 1;
UPDATE users SET profile = profile #- '{address,old_zip}' WHERE id = 1;

構築・集約も豊富です。to_jsonb()(任意の SQL 値を変換)、jsonb_build_object()(キー/値交互)、jsonb_agg()(行を JSON 配列に)、jsonb_object_agg()(ペアを JSON オブジェクトに)。

-- 行を1つの JSON 配列に集約してアプリに返す(N+1 を避ける定番)
SELECT u.id, jsonb_agg(jsonb_build_object('id', o.id, 'amount', o.amount)) AS orders
FROM users u JOIN orders o ON o.user_id = u.id
GROUP BY u.id;

PostgreSQL 18 の追加jsonbnull をスカラ型に**NULL としてキャスト可能に(従来はエラー)。json{b}_strip_nulls配列内の null も除去するオプション**追加。長い JSON 文字列処理の SIMD 高速化


5. SQL/JSON パス:複雑な問い合わせ

単純なキー検索を超える条件(「配列のどれかが5超」など)には SQL/JSON パス言語を使います。演算子は @?(パスが何か返すか=boolean)と @@(パス述語の真偽)。

-- items 配列に price > 1000 の要素が存在するか
SELECT * FROM carts WHERE data @? '$.items[*] ? (@.price > 1000)';

-- 関数版:マッチした値を行として取り出す
SELECT jsonb_path_query(data, '$.items[*] ? (@.price > 1000)') FROM carts;

? (条件)フィルタ式で、@ は「いま評価中の値」を指します。パスには lax(既定)strict があり、lax は構造の不一致を吸収(配列と単一要素を同一視するなど)、strict は構造エラーを投げます。

JSON_TABLE(PG17) を使うと、JSONB を行の集合に展開できます(FROM 句で表として使える)。jsonb の配列をリレーショナルに正規化したいときの強力な道具です。

-- jsonb 配列を行に展開(JSON_TABLE は PostgreSQL 17 以降)
SELECT t.* FROM orders,
  JSON_TABLE(line_items, '$[*]'
    COLUMNS (sku text PATH '$.sku', qty int PATH '$.qty')) AS t;

6. 索引設計:GIN か、式B-tree か(最重要の判断)

JSONB の性能は索引で決まります。判断軸はシンプル——「どんなキー・包含でも柔軟に検索したい」なら GIN、「1つの決まったキーを等値/範囲で引く」なら式B-tree。

GIN:柔軟な検索の主役(2つの演算子クラス)

公式の対応表が判断の核心です。

GIN 演算子クラス対応演算子特徴
jsonb_ops既定? `? ?& @> @? @@`
jsonb_path_ops@> @? @@存在演算子は不可だが索引が小さく、検索が速い

公式:「jsonb_path_ops は同じデータの jsonb_ops 索引より通常ずっと小さく、検索の特異性も高く(頻出キーを含むクエリで特に)、検索性能は既定クラスより良いのが普通」。

-- 万能(存在 ? も使いたい):既定の jsonb_ops
CREATE INDEX idx_docs_data ON docs USING gin (data);

-- 包含 @> 中心で速度・サイズ重視:jsonb_path_ops
CREATE INDEX idx_docs_data_path ON docs USING gin (data jsonb_path_ops);
-- これで WHERE data @> '{"plan":"pro"}' が高速化

jsonb_path_ops の唯一の弱点(公式):{"a": {}} のような値を持たない構造には索引エントリを作らないため、それを探すと全索引スキャンになる。存在演算子(?)が要るかどうかで選びます。

式B-tree:決まったキーを等値/範囲で引くなら最安

「常に data->>'status' で等値検索する」のように問うキーが固定なら、GIN より式B-tree が小さく速い

-- 単一キーの等値/範囲検索:式B-tree が最適(->> はテキストなので必要なら型キャスト)
CREATE INDEX idx_docs_status ON docs ((data ->> 'status'));
-- 数値・日付として範囲検索するならキャストして索引
CREATE INDEX idx_docs_priority ON docs (((data ->> 'priority')::int));

判断まとめ

  • 検索キーが多様・動的、包含/存在を多用 → GIN(既定 jsonb_ops、@>中心なら jsonb_path_ops)。
  • 検索キーが1〜数個に固定、等値/範囲 → 式B-tree(その列を正規化列に昇格する判断もあり)。

7. 設計指針:いつ JSONB を使い、いつ列にするか

JSONB は強力ですが、何でも JSONB は設計の失敗です。公式も「両アプローチの共存を推奨」しつつ、こう釘を刺します。

最大限の柔軟性が欲しい場合でも、JSON ドキュメントはある程度固定された構造を持つことが推奨される。

さらに性能・競合に直結する重要な指摘

どんな更新も行全体に行レベルロックを取る。更新トランザクション間のロック競合を減らすため、JSON ドキュメントは扱いやすいサイズに制限することを検討せよ。理想的には、各 JSON ドキュメントは業務ルール上それ以上分割できない原子的なデータを表すべき。

実務の指針:

こうするこうしない
クエリ条件・結合キー・制約に使う属性は正規化列検索する値を JSONB に埋めて毎回掘る
可変・疎・スキーマ未確定の付帯情報を JSONB に巨大な1ドキュメントに全部詰めて頻繁に部分更新
外部APIのレスポンスなど「原子的な塊」を JSONB に別々に更新したい複数の値を1ドキュメントに同居
NOT NULL・型・CHECK で守れる値は列に昇格整合性が要る値を JSONB 任せにする
-- 良い例:検索/結合/制約に使う列は正規化、可変の付帯情報だけ JSONB
CREATE TABLE products (
  id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  sku         text NOT NULL UNIQUE,           -- 検索・結合キーは列
  price       numeric(12,2) NOT NULL CHECK (price >= 0),
  status      text NOT NULL DEFAULT 'draft',
  attributes  jsonb NOT NULL DEFAULT '{}'     -- カテゴリ毎に異なる可変属性だけ JSONB
);
CREATE INDEX idx_products_attributes ON products USING gin (attributes jsonb_path_ops);

8. 型安全な境界:JSONB は「信頼できない外部入力」

TypeScript から見ると、JSONB 列は**unknownです。DB に何が入っているか型では保証されません。だから読み出した瞬間に検証**するのが鉄則——CLAUDE 流に言えば「境界で narrowing する」。

import { z } from "zod";

// JSONB に入れる構造を単一の真実源として定義
const ProductAttributes = z.object({
  color: z.string().optional(),
  weightGram: z.number().int().positive().optional(),
  certifications: z.array(z.string()).default([]),
});
type ProductAttributes = z.infer<typeof ProductAttributes>;

// DB から読んだ jsonb は unknown。必ず parse して型を確定させる
export function parseAttributes(raw: unknown): ProductAttributes {
  return ProductAttributes.parse(raw); // 不正なら例外。壊れたデータをアプリに流さない
}

// 書き込みもパラメータ化(SQLインジェクション対策)。値は JSON.stringify で渡す
export async function setAttributes(id: number, attrs: ProductAttributes) {
  const valid = ProductAttributes.parse(attrs); // 書く前にも検証
  await pool.query(
    "UPDATE products SET attributes = $1::jsonb WHERE id = $2",
    [JSON.stringify(valid), id],
  );
}

JSONB は柔軟ゆえに「気づかぬうちにスキーマが崩れる」のが最大のリスク。Zod スキーマを単一の真実源にして読み書き両端で検証すれば、柔軟性を保ったまま壊れにくくできます。さらに DB 側でも CHECK (jsonb_typeof(attributes) = 'object') のような最低限の制約を併用すると堅牢です。


9. まとめ

  • 原則 jsonb。json はテキスト保持で再パース、jsonb は分解バイナリで高速+索引可。jsonb はキー順・空白を保持せず、重複キーは最後が勝つ。
  • ->->> を間違えない。包含 @> は順序無視だがネストを平坦化しない、存在 ?トップレベルのみ——この2つの落とし穴を覚える。
  • 索引の判断:柔軟検索=GIN(既定 jsonb_ops、@>中心なら小さく速い jsonb_path_ops)。固定キーの等値/範囲=式B-tree。
  • 設計:検索/結合/制約に使う値は正規化列へ。JSONB は「分割不能な原子データ」に留める(更新は行全体ロック)。
  • 型安全:読み出した jsonb は unknownZod で境界検証し、書き込みはパラメータ化。
  • PG18:jsonb null のスカラキャスト、json_strip_nulls の配列対応、長いJSONのSIMD高速化。

JSONB は「RDB の堅さ」と「スキーマレスの柔らかさ」を1つのテーブルで両立させる道具です。次は、超大規模テーブルを物理的に分割する 宣言的パーティショニング へ。


参考(PostgreSQL 18 公式ドキュメント)

友田

友田 陽大

وزیر (METI) ایوارڈ یافتہ پروڈکٹ کے ڈویلپر۔ TypeScript + Python + AWS کے ساتھ میں SaaS، صنعتی ڈیجیٹل تبدیلی اور پروڈکشن کے قابل جنریٹو اے آئی (RAG) تنہا ابتدا سے انتہا تک فراہم کرتا ہوں — تقاضوں کی تعیین سے انفراسٹرکچر اور آپریشن تک۔

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

پروجیکٹ کی بنیاد پر کام اور تکنیکی مشاورت دونوں کے لیے دستیاب ہوں۔ 30 منٹ کی مفت مشاورت سے آغاز کریں۔

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

یہ بھی پڑھنے کے قابل