はじめに#
リレーショナルデータベースは厳密なスキーマ定義が強みですが、現代のアプリケーション開発では柔軟なデータ構造が求められる場面も少なくありません。ユーザー設定、APIレスポンスのキャッシュ、動的な属性を持つ商品情報など、事前にすべてのカラムを定義することが難しいケースは多々あります。
PostgreSQLのJSONB型は、このような課題を解決する強力なデータ型です。リレーショナルデータベースの堅牢性を維持しながら、NoSQLデータベースのような柔軟性を同時に実現できます。
本記事では、PostgreSQLにおけるJSONB型の基礎から実践的な活用方法までを解説します。JSON型との違い、データ操作の基本、豊富な演算子の使い方、GINインデックスによる高速検索、そしてJSONBを採用すべきケースと避けるべきケースについて、具体的なサンプルコードとともに学んでいきます。
この記事を読むことで、以下のことができるようになります。
- JSON型とJSONB型の違いを理解し、適切に選択できる
- JSONBデータの挿入・取得・更新を自在に行える
- 演算子(
->、->>、@>、?)を使いこなせる
- GINインデックスでJSONB検索を高速化できる
- JSONBを使うべきケースと避けるべきケースを判断できる
前提条件#
- PostgreSQL 14以降がインストールされていること
- psqlまたはGUIツール(pgAdmin、DBeaver等)でデータベースに接続できること
- 基本的なSQL構文(CREATE TABLE、INSERT、SELECT等)を理解していること
JSON型とJSONB型の違い#
PostgreSQLには、JSONデータを格納するための2つのデータ型が用意されています。
JSON型の特徴#
JSON型は、入力されたJSONテキストをそのまま保存します。
1
2
3
4
5
6
7
8
9
10
11
12
|
-- JSON型のテーブル作成
CREATE TABLE json_example (
id SERIAL PRIMARY KEY,
data JSON
);
-- データ挿入
INSERT INTO json_example (data) VALUES ('{"name": "田中", "age": 30}');
-- 取得すると入力時の形式がそのまま返される
SELECT data FROM json_example;
-- 結果: {"name": "田中", "age": 30}
|
JSON型の特徴
- 入力テキストをそのまま保存(ホワイトスペース、キーの順序を保持)
- 重複キーをすべて保持
- 読み取り時に毎回パースが必要(処理が遅い)
- インデックスを作成できない
JSONB型の特徴#
JSONB型は、JSONをバイナリ形式に変換して保存します。
1
2
3
4
5
6
7
8
9
10
11
12
|
-- JSONB型のテーブル作成
CREATE TABLE jsonb_example (
id SERIAL PRIMARY KEY,
data JSONB
);
-- データ挿入
INSERT INTO jsonb_example (data) VALUES ('{"name": "田中", "age": 30}');
-- 取得するとキー順がソートされ、余分なスペースが削除される
SELECT data FROM jsonb_example;
-- 結果: {"age": 30, "name": "田中"}
|
JSONB型の特徴
- バイナリ形式で保存(変換オーバーヘッドあり)
- ホワイトスペースは削除、キー順はソート
- 重複キーは最後の値のみ保持
- 高速な処理(パース不要)
- GINインデックスをサポート
JSON型とJSONB型の比較#
| 特徴 |
JSON |
JSONB |
| 格納形式 |
テキスト |
バイナリ |
| 入力時の変換 |
なし |
あり(やや遅い) |
| 読み取り速度 |
遅い(毎回パース) |
高速(パース不要) |
| ホワイトスペース |
保持 |
削除 |
| キーの順序 |
保持 |
ソート |
| 重複キー |
すべて保持 |
最後の値のみ |
| インデックス |
不可 |
GIN対応 |
| 等価比較 |
不可 |
可能 |
どちらを選ぶべきか#
ほとんどのケースでJSONB型を選択してください。 JSON型を選ぶのは、以下のような特殊なケースに限られます。
- 入力テキストを一切変更せずに保存する必要がある
- 重複キーを保持する必要がある
- データを保存するだけで、検索や加工を行わない
1
2
3
4
5
6
|
-- 実務では基本的にJSONB型を使用
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
attributes JSONB -- 動的な属性情報
);
|
JSONBデータの挿入#
JSONBカラムへのデータ挿入は、JSON文字列をそのまま指定できます。
基本的な挿入#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
-- サンプルテーブルの作成
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
profile JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- シンプルなオブジェクトの挿入
INSERT INTO users (username, profile) VALUES
('tanaka', '{"age": 30, "city": "東京", "interests": ["読書", "映画"]}');
-- ネストしたオブジェクトの挿入
INSERT INTO users (username, profile) VALUES
('suzuki', '{
"age": 25,
"city": "大阪",
"contact": {
"email": "suzuki@example.com",
"phone": "090-1234-5678"
},
"interests": ["プログラミング", "ゲーム", "音楽"]
}');
|
to_jsonb関数を使った挿入#
SQLの値をJSONBに変換する場合は、to_jsonb関数を使用します。
1
2
3
4
5
6
7
8
|
-- レコードをJSONBに変換
SELECT to_jsonb(row('田中太郎', 30, true));
-- 結果: {"f1": "田中太郎", "f2": 30, "f3": true}
-- 型定義付きのレコード変換
CREATE TYPE user_info AS (name TEXT, age INTEGER, active BOOLEAN);
SELECT to_jsonb(ROW('田中太郎', 30, true)::user_info);
-- 結果: {"age": 30, "name": "田中太郎", "active": true}
|
jsonb_build_object関数を使った挿入#
動的にJSONBオブジェクトを構築する場合は、jsonb_build_object関数が便利です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
-- キーと値を交互に指定してオブジェクトを構築
INSERT INTO users (username, profile) VALUES
('yamada', jsonb_build_object(
'age', 35,
'city', '名古屋',
'interests', jsonb_build_array('料理', '旅行')
));
-- 変数やカラム値からオブジェクトを構築
INSERT INTO users (username, profile)
SELECT
'sample_user',
jsonb_build_object(
'registered_at', CURRENT_TIMESTAMP,
'source', 'web',
'preferences', '{}'::jsonb
);
|
JSONBデータの取得#
JSONBからデータを取得するには、専用の演算子と関数を使用します。
基本的な取得演算子#
1
2
|
-- テストデータの確認
SELECT username, profile FROM users;
|
->演算子:JSONB値として取得
1
2
3
4
5
6
7
8
9
10
11
|
-- キーを指定してJSONB値を取得
SELECT profile -> 'city' FROM users WHERE username = 'tanaka';
-- 結果: "東京"(ダブルクォート付き、JSONB型)
-- ネストしたオブジェクトを取得
SELECT profile -> 'contact' FROM users WHERE username = 'suzuki';
-- 結果: {"email": "suzuki@example.com", "phone": "090-1234-5678"}
-- 配列要素を取得(0始まり)
SELECT profile -> 'interests' -> 0 FROM users WHERE username = 'tanaka';
-- 結果: "読書"
|
->>演算子:TEXT値として取得
1
2
3
4
5
6
7
|
-- キーを指定してTEXT値を取得
SELECT profile ->> 'city' FROM users WHERE username = 'tanaka';
-- 結果: 東京(ダブルクォートなし、TEXT型)
-- TEXT型なので文字列比較が可能
SELECT username FROM users WHERE profile ->> 'city' = '東京';
-- 結果: tanaka
|
パス演算子による深い階層へのアクセス#
#>演算子:パス指定でJSONB値を取得
1
2
3
4
5
6
7
|
-- ネストした値をパスで取得
SELECT profile #> '{contact, email}' FROM users WHERE username = 'suzuki';
-- 結果: "suzuki@example.com"
-- 配列内の要素もパスで指定可能
SELECT profile #> '{interests, 1}' FROM users WHERE username = 'tanaka';
-- 結果: "映画"
|
#>>演算子:パス指定でTEXT値を取得
1
2
3
|
-- ネストした値をTEXTとして取得
SELECT profile #>> '{contact, email}' FROM users WHERE username = 'suzuki';
-- 結果: suzuki@example.com
|
演算子の比較まとめ#
| 演算子 |
戻り値の型 |
用途 |
-> |
JSONB |
キーまたはインデックスでJSONB値を取得 |
->> |
TEXT |
キーまたはインデックスでTEXT値を取得 |
#> |
JSONB |
パス配列でJSONB値を取得 |
#>> |
TEXT |
パス配列でTEXT値を取得 |
1
2
3
4
5
6
7
|
-- 使い分けの例
SELECT
profile -> 'age' AS age_jsonb, -- 30(JSONB型)
profile ->> 'age' AS age_text, -- 30(TEXT型)
(profile ->> 'age')::INTEGER AS age_int -- 30(INTEGER型)
FROM users
WHERE username = 'tanaka';
|
JSONBデータの更新#
JSONB型のデータを部分的に更新するには、専用の関数と演算子を使用します。
jsonb_set関数による更新#
jsonb_set関数は、指定したパスの値を新しい値で置き換えます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
-- 構文: jsonb_set(target, path, new_value [, create_if_missing])
-- 既存のキーの値を更新
UPDATE users
SET profile = jsonb_set(profile, '{age}', '31'::jsonb)
WHERE username = 'tanaka';
-- ネストしたキーの値を更新
UPDATE users
SET profile = jsonb_set(profile, '{contact, email}', '"new@example.com"'::jsonb)
WHERE username = 'suzuki';
-- 新しいキーを追加(create_if_missing = true がデフォルト)
UPDATE users
SET profile = jsonb_set(profile, '{occupation}', '"エンジニア"'::jsonb)
WHERE username = 'tanaka';
-- create_if_missing = false の場合、存在しないキーは追加されない
UPDATE users
SET profile = jsonb_set(profile, '{nickname}', '"たなちゃん"'::jsonb, false)
WHERE username = 'tanaka';
-- nicknameが存在しないため、何も変更されない
|
連結演算子(||)による更新#
||演算子は、2つのJSONBオブジェクトをマージします。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
-- 新しいキーを追加
UPDATE users
SET profile = profile || '{"verified": true}'::jsonb
WHERE username = 'tanaka';
-- 複数のキーを一度に追加・更新
UPDATE users
SET profile = profile || '{"age": 32, "status": "active"}'::jsonb
WHERE username = 'tanaka';
-- 既存のキーは上書きされる
SELECT '{"a": 1, "b": 2}'::jsonb || '{"b": 3, "c": 4}'::jsonb;
-- 結果: {"a": 1, "b": 3, "c": 4}
|
削除演算子(-)による更新#
-演算子は、指定したキーまたは要素を削除します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
-- キーを削除(TEXT指定)
UPDATE users
SET profile = profile - 'occupation'
WHERE username = 'tanaka';
-- 複数のキーを削除(TEXT配列指定)
UPDATE users
SET profile = profile - '{verified, status}'::text[]
WHERE username = 'tanaka';
-- 配列から要素を削除(インデックス指定)
UPDATE users
SET profile = jsonb_set(
profile,
'{interests}',
(profile -> 'interests') - 0 -- 最初の要素を削除
)
WHERE username = 'tanaka';
|
パス削除演算子(#-)による更新#
#-演算子は、パスで指定した要素を削除します。
1
2
3
4
5
6
7
8
9
|
-- ネストしたキーを削除
UPDATE users
SET profile = profile #- '{contact, phone}'
WHERE username = 'suzuki';
-- 配列内の要素をパスで削除
UPDATE users
SET profile = profile #- '{interests, 1}'
WHERE username = 'suzuki';
|
添字記法による更新(PostgreSQL 14以降)#
PostgreSQL 14以降では、添字記法(サブスクリプティング)でJSONBを更新できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
-- キーの値を直接更新
UPDATE users
SET profile['age'] = '33'
WHERE username = 'tanaka';
-- ネストした値を更新
UPDATE users
SET profile['contact']['email'] = '"updated@example.com"'
WHERE username = 'suzuki';
-- 新しいキーを追加
UPDATE users
SET profile['new_field'] = '"new_value"'
WHERE username = 'tanaka';
|
JSONB専用演算子#
JSONB型には、検索やフィルタリングに使える強力な演算子が用意されています。
包含演算子(@>、<@)#
@>演算子は、左辺が右辺を包含しているかを判定します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
-- テストデータの準備
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
attributes JSONB
);
INSERT INTO products (name, attributes) VALUES
('ノートPC A', '{"brand": "TechCorp", "specs": {"cpu": "i7", "ram": 16, "storage": "512GB SSD"}, "tags": ["business", "portable"]}'),
('ノートPC B', '{"brand": "GameTech", "specs": {"cpu": "i9", "ram": 32, "storage": "1TB SSD"}, "tags": ["gaming", "high-performance"]}'),
('タブレット C', '{"brand": "TechCorp", "specs": {"cpu": "M1", "ram": 8, "storage": "256GB"}, "tags": ["portable", "creative"]}');
-- 特定のブランドを持つ製品を検索
SELECT name FROM products
WHERE attributes @> '{"brand": "TechCorp"}';
-- 結果: ノートPC A, タブレット C
-- ネストした条件で検索
SELECT name FROM products
WHERE attributes @> '{"specs": {"ram": 16}}';
-- 結果: ノートPC A
-- 配列内の要素を含むか判定
SELECT name FROM products
WHERE attributes @> '{"tags": ["portable"]}';
-- 結果: ノートPC A, タブレット C
-- <@ は逆方向の包含判定
SELECT name FROM products
WHERE '{"brand": "TechCorp"}'::jsonb <@ attributes;
-- 結果: ノートPC A, タブレット C(@>と同じ結果)
|
存在演算子(?、?|、?&)#
キーまたは配列要素の存在を確認します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
-- 特定のキーが存在するか確認(?)
SELECT name FROM products
WHERE attributes ? 'brand';
-- 結果: 全製品
-- 配列内に特定の要素が存在するか確認
SELECT name FROM products
WHERE attributes -> 'tags' ? 'gaming';
-- 結果: ノートPC B
-- いずれかのキーが存在するか確認(?|)
SELECT name FROM products
WHERE attributes -> 'tags' ?| ARRAY['gaming', 'creative'];
-- 結果: ノートPC B, タブレット C
-- すべてのキーが存在するか確認(?&)
SELECT name FROM products
WHERE attributes -> 'tags' ?& ARRAY['portable', 'business'];
-- 結果: ノートPC A
|
演算子一覧#
| 演算子 |
説明 |
例 |
@> |
左辺が右辺を包含 |
'{"a":1,"b":2}' @> '{"a":1}' → true |
<@ |
左辺が右辺に包含される |
'{"a":1}' <@ '{"a":1,"b":2}' → true |
? |
キー/要素が存在 |
'{"a":1}' ? 'a' → true |
| `? |
` |
いずれかのキーが存在 |
?& |
すべてのキーが存在 |
'{"a":1,"b":2}' ?& array['a','b'] → true |
GINインデックスによる高速検索#
大量のJSONBデータを効率的に検索するには、GIN(Generalized Inverted Index)インデックスが不可欠です。
デフォルトのGINインデックス#
1
2
|
-- デフォルトのGINインデックスを作成
CREATE INDEX idx_products_attributes ON products USING GIN (attributes);
|
デフォルトのGINインデックスは、以下の演算子をサポートします。
?、?|、?&(キー存在確認)
@>(包含確認)
@?、@@(JSONPath)
1
2
3
4
5
6
7
8
9
10
|
-- インデックスが使用されるクエリの例
EXPLAIN ANALYZE
SELECT name FROM products
WHERE attributes @> '{"brand": "TechCorp"}';
-- Bitmap Index Scan on idx_products_attributes が使用される
-- キー存在確認もインデックスを使用
EXPLAIN ANALYZE
SELECT name FROM products
WHERE attributes ? 'brand';
|
jsonb_path_ops演算子クラス#
特定の用途では、jsonb_path_ops演算子クラスがより効率的です。
1
2
3
|
-- jsonb_path_opsインデックスを作成
CREATE INDEX idx_products_attributes_path ON products
USING GIN (attributes jsonb_path_ops);
|
jsonb_path_opsの特徴
@>、@?、@@演算子のみサポート(?系はサポートしない)
- インデックスサイズが小さい
- 検索がより高速(特に深いネスト構造)
1
2
3
|
-- jsonb_path_opsが有効なクエリ
SELECT name FROM products
WHERE attributes @> '{"specs": {"cpu": "i7"}}';
|
式インデックス#
特定のキーを頻繁に検索する場合は、式インデックスが効果的です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-- 特定のキーに対するインデックス
CREATE INDEX idx_products_brand ON products
USING GIN ((attributes -> 'tags'));
-- このクエリでインデックスが使用される
SELECT name FROM products
WHERE attributes -> 'tags' ? 'gaming';
-- B-treeインデックスも作成可能(等価比較用)
CREATE INDEX idx_products_brand_btree ON products
((attributes ->> 'brand'));
-- 文字列比較でインデックスを使用
SELECT name FROM products
WHERE attributes ->> 'brand' = 'TechCorp';
|
インデックス選択のガイドライン#
flowchart TD
A[JSONBの検索パターンを分析] --> B{どの演算子を使う?}
B -->|@> のみ| C[jsonb_path_ops]
B -->|? ?| ?& も使う| D[デフォルトGIN]
B -->|特定キーの検索が多い| E[式インデックス]
C --> F[サイズ小・高速]
D --> G[汎用性高い]
E --> H[最も高速だが限定的]実践的なユースケース#
ユーザー設定の管理#
ユーザーごとに異なる設定項目を柔軟に保存できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
CREATE TABLE user_settings (
user_id INTEGER PRIMARY KEY,
settings JSONB DEFAULT '{
"theme": "light",
"language": "ja",
"notifications": {
"email": true,
"push": true
}
}'::jsonb
);
-- 設定を取得
SELECT settings -> 'theme' AS theme FROM user_settings WHERE user_id = 1;
-- 設定を更新
UPDATE user_settings
SET settings = jsonb_set(settings, '{notifications, push}', 'false'::jsonb)
WHERE user_id = 1;
-- 新しい設定を追加
UPDATE user_settings
SET settings = settings || '{"beta_features": true}'::jsonb
WHERE user_id = 1;
|
動的属性を持つ商品カタログ#
カテゴリによって異なる属性を持つ商品を管理できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
CREATE TABLE catalog (
item_id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
category VARCHAR(50) NOT NULL,
attributes JSONB NOT NULL
);
-- 異なる属性を持つ商品を登録
INSERT INTO catalog (name, category, attributes) VALUES
('Tシャツ', '衣類', '{"size": "M", "color": "白", "material": "綿100%"}'),
('スマートフォン', '電子機器', '{"storage": "128GB", "color": "黒", "os": "Android"}'),
('コーヒー豆', '食品', '{"weight": "200g", "origin": "ブラジル", "roast": "中煎り"}');
-- カテゴリ共通の検索(色で検索)
SELECT name, category FROM catalog
WHERE attributes ->> 'color' = '白';
-- カテゴリ固有の検索(ストレージで検索)
SELECT name FROM catalog
WHERE category = '電子機器'
AND attributes @> '{"storage": "128GB"}';
|
APIレスポンスのキャッシュ#
外部APIのレスポンスを構造を保持したまま保存できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
CREATE TABLE api_cache (
cache_key VARCHAR(255) PRIMARY KEY,
response JSONB NOT NULL,
cached_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL
);
-- キャッシュを保存
INSERT INTO api_cache (cache_key, response, expires_at) VALUES
('weather_tokyo', '{"temp": 22, "humidity": 60, "conditions": "晴れ"}',
CURRENT_TIMESTAMP + INTERVAL '1 hour');
-- キャッシュを取得
SELECT response FROM api_cache
WHERE cache_key = 'weather_tokyo'
AND expires_at > CURRENT_TIMESTAMP;
-- 特定の条件でキャッシュを検索
SELECT cache_key, response ->> 'temp' AS temperature
FROM api_cache
WHERE response @> '{"conditions": "晴れ"}';
|
イベントログの保存#
構造が変化する可能性のあるイベントデータを柔軟に保存できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
CREATE TABLE event_logs (
log_id BIGSERIAL PRIMARY KEY,
event_type VARCHAR(50) NOT NULL,
event_data JSONB NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- インデックスを作成
CREATE INDEX idx_event_logs_type ON event_logs (event_type);
CREATE INDEX idx_event_logs_data ON event_logs USING GIN (event_data);
-- 様々なイベントを記録
INSERT INTO event_logs (event_type, event_data) VALUES
('user_login', '{"user_id": 123, "ip": "192.168.1.1", "device": "mobile"}'),
('purchase', '{"user_id": 123, "items": [{"id": 1, "qty": 2}], "total": 5000}'),
('error', '{"code": "E001", "message": "Connection timeout", "service": "payment"}');
-- 特定ユーザーのイベントを検索
SELECT event_type, event_data, created_at
FROM event_logs
WHERE event_data @> '{"user_id": 123}'
ORDER BY created_at DESC;
|
JSONBを使うべきケースと避けるべきケース#
JSONBを使うべきケース#
1. スキーマが動的に変化するデータ
1
2
3
4
5
6
|
-- ユーザーが自由に定義するカスタムフィールド
CREATE TABLE custom_entities (
entity_id SERIAL PRIMARY KEY,
entity_type VARCHAR(50) NOT NULL,
data JSONB NOT NULL
);
|
2. 外部システムからのデータ保存
1
2
3
4
5
6
|
-- 外部APIのレスポンスをそのまま保存
CREATE TABLE external_data (
source VARCHAR(100) NOT NULL,
raw_data JSONB NOT NULL,
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
|
3. 階層構造を持つデータ
1
2
3
4
5
6
|
-- 組織階層やカテゴリツリー
CREATE TABLE categories (
category_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
metadata JSONB DEFAULT '{}'
);
|
4. 疎なデータ(多くのNULLを含む可能性があるデータ)
1
2
3
4
5
6
|
-- 製品ごとに異なるオプション属性
-- 通常のカラムにすると多数のNULLが発生する場合に有効
CREATE TABLE product_options (
product_id INTEGER PRIMARY KEY,
options JSONB DEFAULT '{}'
);
|
JSONBを避けるべきケース#
1. 頻繁にJOINするデータ
1
2
3
4
5
6
7
8
9
10
11
12
13
|
-- 避けるべき例: 外部キーをJSONB内に保存
-- ❌ 悪い例
CREATE TABLE orders_bad (
order_id SERIAL PRIMARY KEY,
data JSONB -- {"user_id": 1, "items": [...]}
);
-- ✅ 良い例: リレーショナルな設計
CREATE TABLE orders_good (
order_id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(user_id),
metadata JSONB DEFAULT '{}' -- 補助的なデータのみ
);
|
2. 集計が頻繁なデータ
1
2
3
4
5
6
7
8
|
-- 避けるべき例: 集計対象をJSONBに保存
-- ❌ 悪い例
SELECT SUM((data ->> 'amount')::NUMERIC) -- 毎回キャストが必要
FROM transactions;
-- ✅ 良い例: 集計対象は通常カラム
SELECT SUM(amount)
FROM transactions;
|
3. 一意性制約が必要なデータ
1
2
3
4
5
6
7
8
9
|
-- ❌ JSONB内の値に一意性制約は設定できない
-- email がJSONB内にある場合、重複チェックが困難
-- ✅ 良い例: 一意性が必要なフィールドは通常カラムに
CREATE TABLE accounts (
account_id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
profile JSONB DEFAULT '{}'
);
|
4. 構造が固定されているデータ
1
2
3
4
5
6
7
8
9
|
-- 住所のように構造が固定されているデータは通常カラムが適切
-- ✅ 良い例
CREATE TABLE addresses (
address_id SERIAL PRIMARY KEY,
postal_code VARCHAR(10),
prefecture VARCHAR(20),
city VARCHAR(50),
street VARCHAR(100)
);
|
設計判断フローチャート#
flowchart TD
A[新しいデータ項目を追加] --> B{構造は固定?}
B -->|はい| C{JOINや集計が多い?}
B -->|いいえ/不明| D[JSONB型を検討]
C -->|はい| E[通常カラムを使用]
C -->|いいえ| F{一意性制約が必要?}
F -->|はい| E
F -->|いいえ| D
D --> G{パフォーマンス要件}
G -->|高速検索必須| H[GINインデックスを作成]
G -->|検索少ない| I[インデックスなしで運用]まとめ#
PostgreSQLのJSONB型は、リレーショナルデータベースの堅牢性を維持しながら、スキーマレスなデータを柔軟に扱える強力な機能です。
本記事で学んだ主要ポイントを振り返ります。
- JSON型とJSONB型の違い: ほとんどのケースでJSONB型を選択する。バイナリ形式で保存され、高速な処理とインデックスをサポート
- データ操作:
->、->>演算子で値を取得し、jsonb_set関数や||演算子で更新する
- JSONB専用演算子:
@>(包含)、?(存在確認)を使って効率的に検索する
- GINインデックス: 大量データの検索にはGINインデックスが不可欠。用途に応じて
jsonb_path_opsや式インデックスを使い分ける
- 設計判断: 動的スキーマや疎なデータにはJSONBが有効だが、JOINや集計が多いデータには通常カラムを使用する
JSONBを適切に活用することで、アプリケーションの要件変化に柔軟に対応しながら、PostgreSQLの持つトランザクション整合性やSQLの表現力を最大限に活かすことができます。
参考リンク#