PostgreSQL: PostgreSQLのJSONとJSONBデータ処理

最終更新:2026-08-26

1. 学習内容


2. ストーリー

BobはSaaSプラットフォームのバックエンドエンジニアです。プラットフォームではユーザー設定と商品属性を保存する必要がありますが、これらのフィールドは顧客ごとに異なります:

従来のリレーショナルモデルでは、新しいフィールドごとにALTER TABLEが必要です。BobはJSONBを使用してこれらの動的フィールドを1つのテーブルに保存することを選択しました。柔軟で効率的です。


3. 概念: JSONとJSONBの比較

(1) 2つのJSON型の比較

観点 JSON JSONB
保存形式 テキストとして保存、そのまま保持 バイナリとして保存、パース後に保存
書き込み速度 速い(パース不要) 遅い(パースと変換が必要)
検索速度 遅い(毎回パース) 非常に速い(既にツリー構造にパース済み)
インデックス対応 ネイティブインデックスなし GINインデックス対応
空白/順序 元の空白とキー順序を保持 保持しない。キーはアルファベット順にソート
重複キー すべての重複キーを保持 最後の値のみ保持
推奨用途 保存のみ、検索なし 大多数のケース

▶ サンプル: JSONは空白を保持、JSONBは保持しない

SQL
SELECT '{"name": "Alice", "age": 30}'::json;
-- {"name": "Alice", "age": 30}

SELECT '{"name": "Alice", "age": 30}'::jsonb;
-- {"age": 30, "name": "Alice"}

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ サンプル: JSONBカラムを持つテーブルを作成する

SQL
CREATE TABLE users (
  user_id SERIAL PRIMARY KEY,
  username TEXT NOT NULL,
  profile JSONB NOT NULL DEFAULT '{}'
);

INSERT INTO users (username, profile) VALUES
  ('alice', '{"theme": "dark", "language": "en", "notifications": true}'),
  ('bob', '{"timezone": "UTC-5", "currency": "USD", "dashboard_layout": "grid"}');

Output:

TEXT 📖 参照専用
INSERT 0 1

4. 概念: JSON演算子

(1) 基本的な抽出演算子

演算子 右オペランド 戻り値の型 説明
-> int JSON/JSONB インデックスで配列要素を取得 '[1,2,3]'::jsonb -> 12
-> text JSON/JSONB キーでオブジェクト値を取得 '{"a":1}'::jsonb -> 'a'1
->> int text インデックスで配列要素を取得(テキスト) '[1,2,3]'::jsonb ->> 1"2"
->> text text キーでオブジェクト値を取得(テキスト) '{"a":1}'::jsonb ->> 'a'"1"

▶ サンプル: ネストされたフィールドを抽出

SQL
SELECT profile -> 'theme' AS theme_json,
       profile ->> 'theme' AS theme_text
FROM users
WHERE username = 'alice';

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

(2) パス抽出演算子

演算子 右オペランド 戻り値の型 説明
#> text[] JSON/JSONB パスで値を取得(JSON形式)
#>> text[] text パスで値を取得(テキスト形式)

▶ サンプル: パス抽出

SQL
SELECT profile #> '{address,city}' AS city_json,
       profile #>> '{address,city}' AS city_text
FROM users
WHERE profile ? 'address';

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

(3) 包含と存在演算子(JSONBのみ)

演算子 説明
@> 左側が右側を含むか '{"a":1,"b":2}'::jsonb @> '{"a":1}' → true
<@ 左側が右側に含まれるか '{"a":1}'::jsonb <@ '{"a":1,"b":2}' → true
? キーが存在するか '{"a":1}'::jsonb ? 'a' → true
`? ` いずれかのキーが存在するか
?& すべてのキーが存在するか '{"a":1}'::jsonb ?& array['a','b'] → false

▶ サンプル: 包含検索 — ダークテーマが有効な全ユーザーを検索

SQL
SELECT username, profile
FROM users
WHERE profile @> '{"theme": "dark"}';

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ サンプル: キー存在検索 — タイムゾーンを設定したユーザーを検索

SQL
SELECT username, profile ->> 'timezone' AS tz
FROM users
WHERE profile ? 'timezone';

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ サンプル: 複数キー検索 — タイムゾーンまたは通貨を設定したユーザーを検索

SQL
SELECT username
FROM users
WHERE profile ?| array['timezone', 'currency'];

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

5. 概念: JSONB関数

(1) 検索と抽出の関数

関数 戻り値の型 説明
jsonb_path_query(data, path) setof jsonb JSONPATHで検索、全一致を返す
jsonb_array_elements(data) setof jsonb 配列を行セットに展開
jsonb_each(data) setof (key, 値) オブジェクトをキーと値のペアに展開
jsonb_object_keys(data) setof text すべてのトップレベルキーを返す
jsonb_typeof(data) text JSON値の型を返す

▶ サンプル: JSON配列を行に展開

SQL
CREATE TABLE products (
  product_id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  attributes JSONB NOT NULL DEFAULT '{}'
);

INSERT INTO products (name, attributes) VALUES
  ('T-Shirt', '{"colors": ["red", "blue", "green"], "sizes": ["S", "M", "L"]}'),
  ('Laptop', '{"colors": ["silver", "black"], "warranty_years": 2}');

SELECT product_id, name,
       jsonb_array_elements_text(attributes -> 'colors') AS color
FROM products;

Output:

TEXT 📖 参照専用
INSERT 0 1

▶ サンプル: オブジェクトをキーと値のペアに展開

SQL
SELECT username,
       (jsonb_each(profile)).key AS config_key,
       (jsonb_each(profile)).value AS config_value
FROM users;

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

(2) 変更関数

関数 説明
jsonb_set(target, path, new_value) 指定パスの値を設定
jsonb_insert(target, path, new_value [, before]) 指定パスに新しい値を挿入
target - key トップレベルキーを削除
target - path_array 指定パスを削除
jsonb_pretty(data) 整形して出力

▶ サンプル: ユーザー設定を変更

SQL
-- フィールドを追加または更新
UPDATE users
SET profile = jsonb_set(profile, '{language}', '"zh"')
WHERE username = 'alice';

-- ネストされたフィールドを追加
UPDATE users
SET profile = jsonb_set(profile, '{address,city}', '"New York"')
WHERE username = 'alice';

-- フィールドを削除
UPDATE users
SET profile = profile - 'notifications'
WHERE username = 'alice';

Output:

TEXT 📖 参照専用
-- SQL statement executed successfully

▶ サンプル: JSON配列に要素を追加

SQL
-- 末尾に追加(パスは既存の配列を指し、最後の要素の後に挿入)
UPDATE products
SET attributes = jsonb_set(
  attributes, '{colors}',
  (attributes -> 'colors') || '"yellow"'
)
WHERE name = 'T-Shirt';

Output:

TEXT 📖 参照専用
INSERT 0 1

▶ サンプル: JSONを整形表示

SQL
SELECT jsonb_pretty(profile) FROM users WHERE username = 'alice';
TEXT 📖 参照専用
{
    "theme": "dark",
    "language": "zh",
    "address": {
        "city": "New York"
    }
}

6. 概念: JSONBインデックス

(1) GINインデックスでJSONBクエリを高速化

GINインデックス種類 サポート演算子 説明
jsonb_ops(デフォルト) @> ? `? ?&`
jsonb_path_ops @> より小さく高速なインデックス。包含検索のみ対応

▶ サンプル: GINインデックスを作成

SQL
-- デフォルトGINインデックス(@>, ?, ?|, ?&をサポート)
CREATE INDEX idx_users_profile ON users USING gin (profile);

-- Path ops GINインデックス(@>のみ、より小さく高速)
CREATE INDEX idx_users_profile_path ON users USING gin (profile jsonb_path_ops);

Output:

TEXT 📖 参照専用
CREATE TABLE

▶ サンプル: インデックス有無でのクエリパフォーマンス比較

SQL
-- インデックスなし: 逐次スキャン
EXPLAIN ANALYZE
SELECT * FROM users WHERE profile @> '{"theme": "dark"}';

-- GINインデックス作成後: ビットマップインデックススキャン
CREATE INDEX idx_users_profile ON users USING gin (profile);
EXPLAIN ANALYZE
SELECT * FROM users WHERE profile @> '{"theme": "dark"}';

Output:

TEXT 📖 参照専用
CREATE TABLE
検索方法 GINインデックス使用? 説明
profile @> '{"theme":"dark"}' はい 包含検索 — GINの最適ケース
profile ->> 'theme' = 'dark' いいえ 抽出してから比較 — B-tree式インデックスが必要
profile ? 'theme' はい キー存在検索

▶ サンプル: B-tree式インデックスで抽出クエリを高速化

SQL
-- ->> 演算子を使用するクエリ用
CREATE INDEX idx_users_theme ON users ((profile ->> 'theme'));
SELECT * FROM users WHERE profile ->> 'theme' = 'dark'; -- インデックスを使用

Output:

TEXT 📖 参照専用
CREATE TABLE

7. 概念: JSONPATH(SQL/JSON標準)

(1) JSONPATH構文

PostgreSQL 12以降はSQL/JSON標準のJSONPATHをサポートします。XPathに似ており、複雑なJSONクエリに使用します。

構文 説明
$.key ルートオブジェクトのキー $.theme
$.array[*] 配列を反復 $.colors[*]
$.nested.key ネストされたアクセス $.address.city
? (condition) フィルタ $.items[*] ? (@.price > 100)
@ 現在の要素 @.name

▶ サンプル: jsonb_path_queryで検索

SQL
SELECT jsonb_path_query(profile, '$.theme') AS theme
FROM users
WHERE username = 'alice';

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ サンプル: フィルタ条件付きJSONPATHクエリ

SQL
-- 保証期間が1年超の商品
SELECT name,
       jsonb_path_query(attributes, '$.warranty_years') AS warranty
FROM products
WHERE jsonb_path_exists(attributes, '$.warranty_years ? (@ > 1)');

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ サンプル: 配列要素を反復

SQL
SELECT name,
       jsonb_path_query(attributes, '$.colors[*]') AS color
FROM products;

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
関数 戻り値の型 説明
jsonb_path_query(data, path) setof jsonb すべての一致を返す
jsonb_path_query_array(data, path) jsonb 一致をJSON配列として返す
jsonb_path_query_first(data, path) jsonb 最初の一致を返す
jsonb_path_exists(data, path) ブール値 一致が存在するか

8. 概念: JSONBとリレーショナルデータのハイブリッド設計

(1) JSONBを使用する場合とリレーショナルカラムを使用する場合

ケース 推奨 理由
頻繁に検索/ソート/結合されるフィールド リレーショナルカラム + B-treeインデックス 最高のパフォーマンス
固定構造でビジネスロジックに関与するフィールド リレーショナルカラム 型安全、完全な制約
顧客ごとに構造が異なるフィールド JSONB + GINインデックス 柔軟、ALTER TABLE不要
時折の補足情報検索 JSONB メインテーブル構造を汚染しない
正確な型制約が必要な動的フィールド JSONB + CHECK制約 柔軟性と安全性のバランス

▶ サンプル: ハイブリッド設計 — 商品テーブル

SQL
CREATE TABLE products_v2 (
  product_id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  category TEXT NOT NULL,              -- 固定カラム、インデックス付き
  price NUMERIC(10,2) NOT NULL,        -- 固定カラム、インデックス付き
  stock INT NOT NULL DEFAULT 0,         -- 固定カラム、インデックス付き
  attributes JSONB NOT NULL DEFAULT '{}', -- 動的属性
  metadata JSONB DEFAULT '{}'          -- ほとんど検索されないメタ情報
);

CREATE INDEX idx_products_category ON products_v2 (category);
CREATE INDEX idx_products_attrs ON products_v2 USING gin (attributes jsonb_path_ops);

Output:

TEXT 📖 参照専用
CREATE TABLE

▶ サンプル: JSONB CHECK制約でデータ品質を確保

SQL
ALTER TABLE products_v2
ADD CONSTRAINT chk_attributes_schema
CHECK (
  jsonb_typeof(attributes -> 'colors') = 'array'
  AND attributes ? 'colors'
);

Output:

TEXT 📖 参照専用
-- SQL statement executed successfully

▶ サンプル: JSONB結合クエリ

SQL
-- 特定の属性を持つ商品の注文を検索
SELECT o.order_id, o.customer_id, p.name
FROM orders o
JOIN products_v2 p ON o.product_id = p.product_id
WHERE p.attributes @> '{"warranty_years": 2}';

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

9. JSON保存と検索フロー

100%
flowchart TD
    A["JSONテキスト入力"] --> B{"対象の型は?"}
    B -->|"json"| C["そのまま保存<br/>パースオーバーヘッドなし"]
    B -->|"jsonb"| D["パースして<br/>バイナリツリーに変換"]
    D --> E["JSONBとして保存<br/>キーはソート済み、空白なし"]
    E --> F{"クエリ種類は?"}
    F -->|"@> 包含"| G["GINインデックススキャン<br/>高速パス"]
    F -->|"->> 抽出 + 比較"| H["B-tree式インデックス<br/>または逐次スキャン"]
    F -->|"jsonpath"| I["JSONPATHエンジン<br/>PG 12+"]
    G --> J["結果を返す"]
    H --> J
    I --> J
    C --> K["毎回パース<br/>遅い、インデックスなし"]
    K --> J

10. 実践: SaaSプラットフォームのユーザー設定と商品属性システム

Bobは完全なSaaSプラットフォームのデータ保存ソリューションを実装し、柔軟なユーザー設定と商品属性管理をサポートする必要があります。

SQL
-- ステップ1: JSONB付きのコアテーブルを作成
CREATE TABLE saas_users (
  user_id SERIAL PRIMARY KEY,
  email TEXT NOT NULL UNIQUE,
  name TEXT NOT NULL,
  plan TEXT NOT NULL DEFAULT 'free',
  config JSONB NOT NULL DEFAULT '{}',
  created_at TIMESTAMP DEFAULT now()
);

CREATE TABLE saas_products (
  product_id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  category TEXT NOT NULL,
  price NUMERIC(10,2) NOT NULL,
  specs JSONB NOT NULL DEFAULT '{}',
  tags JSONB NOT NULL DEFAULT '[]'
);

-- ステップ2: サンプルデータを挿入
INSERT INTO saas_users (email, name, plan, config) VALUES
  ('alice@corp.com', 'Alice', 'pro',
   '{"theme":"dark","language":"en","notifications":{"email":true,"sms":false},"sidebar":["dashboard","reports"]}'),
  ('bob@corp.com', 'Bob', 'enterprise',
   '{"theme":"light","language":"zh","notifications":{"email":true,"sms":true},"sidebar":["dashboard","admin","billing"]}');

INSERT INTO saas_products (name, category, price, specs, tags) VALUES
  ('Pro Widget', 'widget', 49.99,
   '{"weight_kg":0.5,"colors":["red","blue"],"warranty_years":3}',
   '["popular","new"]'),
  ('Mega Gadget', 'gadget', 199.99,
   '{"weight_kg":2.0,"colors":["silver","black"],"voltage":"220V"}',
   '["premium","bestseller"]');

-- ステップ3: インデックスを作成
CREATE INDEX idx_saas_users_config ON saas_users USING gin (config);
CREATE INDEX idx_saas_products_specs ON saas_products USING gin (specs jsonb_path_ops);
CREATE INDEX idx_saas_products_tags ON saas_products USING gin (tags);
CREATE INDEX idx_saas_products_category ON saas_products (category);

-- ステップ4: クエリ例
-- メール通知が有効なユーザーを検索
SELECT name, config ->> 'theme' AS theme
FROM saas_users
WHERE config @> '{"notifications":{"email":true}}';

-- 赤色が利用可能な商品を検索
SELECT name, price
FROM saas_products
WHERE specs -> 'colors' @> '["red"]';

-- 特定のタグを持つ商品を検索
SELECT name
FROM saas_products
WHERE tags @> '["premium"]';

-- ユーザー設定を更新(新しいフィールドを追加)
UPDATE saas_users
SET config = jsonb_set(config, '{timezone}', '"America/New_York"')
WHERE email = 'alice@corp.com';

-- 設定フィールドを削除
UPDATE saas_users
SET config = config - 'language'
WHERE email = 'bob@corp.com';

-- 分析用に商品タグを展開
SELECT name, jsonb_array_elements_text(tags) AS tag
FROM saas_products;

-- ユーザー設定を整形表示
SELECT name, jsonb_pretty(config) FROM saas_users WHERE plan = 'pro';

❓ よくある質問

Q JSONとJSONBのどちらを選ぶべきですか?
A 大多数のケースではJSONBを選んでください。JSONBは検索が速く、インデックスをサポートし、より豊富な演算子を持ちます。JSONは元のテキスト形式(空白、キー順序、重複キー)を保持する必要がある場合や、保存のみで検索しない場合にのみ使用してください。
Q JSONBはリレーショナルテーブルを置き換えられますか?
A 完全には置き換えられません。頻繁に検索/ソート/結合されるフィールドはリレーショナルカラムを使用すべきです。JSONBは構造が可変な補足データに適しています。ハイブリッド設計がベストプラクティスです。
Q jsonb_setとjsonb_insertの違いは何ですか?
A jsonb_setは既存パスの値を置き換え、パスが存在しない場合は作成します。jsonb_insertは配列の指定位置に新しい要素を挿入し(beforeパラメータで前後を制御)、キーが既に存在する場合は置き換えません。
Q GINインデックスとB-tree式インデックスの選び方は?
A 包含検索(@>)にはGINを、等価検索(->> 'key' = '値')にはB-tree式インデックスを使用します。両者は共存可能で、異なるクエリパターンをカバーします。
Q JSONPATHと従来の演算子の選び方は?
A 単純なクエリには演算子の方が簡潔です。複雑なネスト検索やフィルタ条件にはJSONPATHの方が強力です。JSONPATHはSQL/JSON標準であり、移植性が高くなります。
Q JSONBフィールドにCHECK制約を設定できますか?
A はい。CHECK制約内でjsonb_typeof()や?演算子などを使用してJSONB構造を検証できます。例: CHECK (jsonb_typeof(attributes -> 'colors') = '配列')。
Q 大量のJSONB更新はパフォーマンス問題を引き起こしますか?
A はい。JSONBの更新は値全体を置き換えるため(MVCCが新しいバージョンを作成)、大きなJSONB値を頻繁に更新すると多数のデッドタプルが生成されます。頻繁に更新されるフィールドはリレーショナルカラムに分割し、JSONBは更新頻度の低い補足データの保存に使用することをおすすめします。

📖 まとめ


📝 練習問題

  1. app_name(TEXT)とsettings(JSONB)カラムを持つapp_settingsテーブルを作成し、2行を挿入してから->>を使用して設定項目の値を検索してください。

  2. ⭐⭐ saas_productsテーブルにGINインデックスを作成し、specswarranty_years > 2を持つ全商品の名前を検索し、jsonb_prettyspecsを整形表示するクエリを記述してください。

  3. ⭐⭐⭐ 注文テーブル用のJSONBハイブリッドスキームを設計: order_id/customer_id/total_amount/status/created_atを固定カラムとし、JSONBカラムextraにクーポン情報(coupon_code/discount_percent)と配送メモ(delivery_notes)を保存します。extra付きの注文を挿入し、@>で特定のクーポンを使用した注文を検索し、jsonb_setで既存注文にgift_wrap: trueを追加するクエリを記述してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%