PostgreSQL: PostgreSQLのJSONとJSONBデータ処理
最終更新:2026-08-26
1. 学習内容
- JSONとJSONBの違い、JSONBの利点を理解する
- JSON演算子を使用したデータの抽出とフィルタリング
- JSONB関数を使用したデータの検索、変更、生成
- GINインデックスを作成してJSONBクエリを高速化
- JSONPATH(SQL/JSON標準)を使用した複雑なクエリ
- JSONBとリレーショナルデータを組み合わせたハイブリッド設計パターン
2. ストーリー
BobはSaaSプラットフォームのバックエンドエンジニアです。プラットフォームではユーザー設定と商品属性を保存する必要がありますが、これらのフィールドは顧客ごとに異なります:
- 顧客Aのユーザー設定には
theme、language、notificationsがある - 顧客Bのユーザー設定には
timezone、currency、dashboard_layoutがある - 商品属性はさらに多様: 衣類には
size/color、電子機器にはwarranty/voltage
従来のリレーショナルモデルでは、新しいフィールドごとにALTER TABLEが必要です。BobはJSONBを使用してこれらの動的フィールドを1つのテーブルに保存することを選択しました。柔軟で効率的です。
3. 概念: JSONとJSONBの比較
(1) 2つのJSON型の比較
| 観点 | JSON | JSONB |
|---|---|---|
| 保存形式 | テキストとして保存、そのまま保持 | バイナリとして保存、パース後に保存 |
| 書き込み速度 | 速い(パース不要) | 遅い(パースと変換が必要) |
| 検索速度 | 遅い(毎回パース) | 非常に速い(既にツリー構造にパース済み) |
| インデックス対応 | ネイティブインデックスなし | GINインデックス対応 |
| 空白/順序 | 元の空白とキー順序を保持 | 保持しない。キーはアルファベット順にソート |
| 重複キー | すべての重複キーを保持 | 最後の値のみ保持 |
| 推奨用途 | 保存のみ、検索なし | 大多数のケース |
▶ サンプル: JSONは空白を保持、JSONBは保持しない
SELECT '{"name": "Alice", "age": 30}'::json;
-- {"name": "Alice", "age": 30}
SELECT '{"name": "Alice", "age": 30}'::jsonb;
-- {"age": 30, "name": "Alice"}
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ サンプル: JSONBカラムを持つテーブルを作成する
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:
INSERT 0 1
4. 概念: JSON演算子
(1) 基本的な抽出演算子
| 演算子 | 右オペランド | 戻り値の型 | 説明 | 例 |
|---|---|---|---|---|
-> |
int | JSON/JSONB | インデックスで配列要素を取得 | '[1,2,3]'::jsonb -> 1 → 2 |
-> |
text | JSON/JSONB | キーでオブジェクト値を取得 | '{"a":1}'::jsonb -> 'a' → 1 |
->> |
int | text | インデックスで配列要素を取得(テキスト) | '[1,2,3]'::jsonb ->> 1 → "2" |
->> |
text | text | キーでオブジェクト値を取得(テキスト) | '{"a":1}'::jsonb ->> 'a' → "1" |
▶ サンプル: ネストされたフィールドを抽出
SELECT profile -> 'theme' AS theme_json,
profile ->> 'theme' AS theme_text
FROM users
WHERE username = 'alice';
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(2) パス抽出演算子
| 演算子 | 右オペランド | 戻り値の型 | 説明 |
|---|---|---|---|
#> |
text[] | JSON/JSONB | パスで値を取得(JSON形式) |
#>> |
text[] | text | パスで値を取得(テキスト形式) |
▶ サンプル: パス抽出
SELECT profile #> '{address,city}' AS city_json,
profile #>> '{address,city}' AS city_text
FROM users
WHERE profile ? 'address';
Output:
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 |
▶ サンプル: 包含検索 — ダークテーマが有効な全ユーザーを検索
SELECT username, profile
FROM users
WHERE profile @> '{"theme": "dark"}';
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ サンプル: キー存在検索 — タイムゾーンを設定したユーザーを検索
SELECT username, profile ->> 'timezone' AS tz
FROM users
WHERE profile ? 'timezone';
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ サンプル: 複数キー検索 — タイムゾーンまたは通貨を設定したユーザーを検索
SELECT username
FROM users
WHERE profile ?| array['timezone', 'currency'];
Output:
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配列を行に展開
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:
INSERT 0 1
▶ サンプル: オブジェクトをキーと値のペアに展開
SELECT username,
(jsonb_each(profile)).key AS config_key,
(jsonb_each(profile)).value AS config_value
FROM users;
Output:
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) |
整形して出力 |
▶ サンプル: ユーザー設定を変更
-- フィールドを追加または更新
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:
-- SQL statement executed successfully
▶ サンプル: JSON配列に要素を追加
-- 末尾に追加(パスは既存の配列を指し、最後の要素の後に挿入)
UPDATE products
SET attributes = jsonb_set(
attributes, '{colors}',
(attributes -> 'colors') || '"yellow"'
)
WHERE name = 'T-Shirt';
Output:
INSERT 0 1
▶ サンプル: JSONを整形表示
SELECT jsonb_pretty(profile) FROM users WHERE username = 'alice';
{
"theme": "dark",
"language": "zh",
"address": {
"city": "New York"
}
}
6. 概念: JSONBインデックス
(1) GINインデックスでJSONBクエリを高速化
| GINインデックス種類 | サポート演算子 | 説明 |
|---|---|---|
jsonb_ops(デフォルト) |
@> ? `? |
?&` |
jsonb_path_ops |
@> |
より小さく高速なインデックス。包含検索のみ対応 |
▶ サンプル: GINインデックスを作成
-- デフォルト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:
CREATE TABLE
▶ サンプル: インデックス有無でのクエリパフォーマンス比較
-- インデックスなし: 逐次スキャン
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:
CREATE TABLE
| 検索方法 | GINインデックス使用? | 説明 |
|---|---|---|
profile @> '{"theme":"dark"}' |
はい | 包含検索 — GINの最適ケース |
profile ->> 'theme' = 'dark' |
いいえ | 抽出してから比較 — B-tree式インデックスが必要 |
profile ? 'theme' |
はい | キー存在検索 |
▶ サンプル: B-tree式インデックスで抽出クエリを高速化
-- ->> 演算子を使用するクエリ用
CREATE INDEX idx_users_theme ON users ((profile ->> 'theme'));
SELECT * FROM users WHERE profile ->> 'theme' = 'dark'; -- インデックスを使用
Output:
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で検索
SELECT jsonb_path_query(profile, '$.theme') AS theme
FROM users
WHERE username = 'alice';
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ サンプル: フィルタ条件付きJSONPATHクエリ
-- 保証期間が1年超の商品
SELECT name,
jsonb_path_query(attributes, '$.warranty_years') AS warranty
FROM products
WHERE jsonb_path_exists(attributes, '$.warranty_years ? (@ > 1)');
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ サンプル: 配列要素を反復
SELECT name,
jsonb_path_query(attributes, '$.colors[*]') AS color
FROM products;
Output:
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制約 | 柔軟性と安全性のバランス |
▶ サンプル: ハイブリッド設計 — 商品テーブル
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:
CREATE TABLE
▶ サンプル: JSONB CHECK制約でデータ品質を確保
ALTER TABLE products_v2
ADD CONSTRAINT chk_attributes_schema
CHECK (
jsonb_typeof(attributes -> 'colors') = 'array'
AND attributes ? 'colors'
);
Output:
-- SQL statement executed successfully
▶ サンプル: JSONB結合クエリ
-- 特定の属性を持つ商品の注文を検索
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:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
9. JSON保存と検索フロー
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プラットフォームのデータ保存ソリューションを実装し、柔軟なユーザー設定と商品属性管理をサポートする必要があります。
-- ステップ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';
❓ よくある質問
📖 まとめ
- JSONBはバイナリ保存のJSONで、検索が速く、インデックスをサポート。デフォルトの選択として推奨
->はJSON型、->>はテキスト型を返す。#>/#>>はパスで抽出- 包含演算子
@>とGINインデックスの組み合わせがJSONBクエリの最適な組み合わせ jsonb_set/jsonb_insert/-で追加/変更/削除。jsonb_array_elements/jsonb_eachで展開- JSONPATH(PG 12+)はSQL/JSON標準の複雑なクエリ機能を提供
- ハイブリッド設計: 固定フィールドはリレーショナルカラム、動的フィールドはJSONB、CHECK制約で品質を確保
📝 練習問題
-
⭐
app_name(TEXT)とsettings(JSONB)カラムを持つapp_settingsテーブルを作成し、2行を挿入してから->>を使用して設定項目の値を検索してください。 -
⭐⭐
saas_productsテーブルにGINインデックスを作成し、specsにwarranty_years > 2を持つ全商品の名前を検索し、jsonb_prettyでspecsを整形表示するクエリを記述してください。 -
⭐⭐⭐ 注文テーブル用のJSONBハイブリッドスキームを設計:
order_id/customer_id/total_amount/status/created_atを固定カラムとし、JSONBカラムextraにクーポン情報(coupon_code/discount_percent)と配送メモ(delivery_notes)を保存します。extra付きの注文を挿入し、@>で特定のクーポンを使用した注文を検索し、jsonb_setで既存注文にgift_wrap: trueを追加するクエリを記述してください。