PostgreSQL JSONB実践ガイド — 検索・更新・インデックス

目次
PostgreSQLのJSONB型は、スキーマレスなデータをリレーショナルDBで扱える強力な機能です。本記事ではJSONBの実践的な操作パターンをまとめます。
JSON vs JSONB
| 項目 | JSON | JSONB |
|---|---|---|
| 格納形式 | テキスト | バイナリ |
| 書込み速度 | 速い | やや遅い |
| 読込み速度 | 遅い | 速い |
| インデックス | 不可 | GIN対応 |
| 重複キー | 保持 | 最後の値を保持 |
| 空白・順序 | 保持 | 正規化 |
基本的に JSONB を推奨 します。読み取りが高速でインデックスも使えます。
テーブル設計
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
attributes JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
INSERT INTO products (name, attributes) VALUES
('MacBook Pro', '{"brand": "Apple", "cpu": "M5 Pro", "ram": 36, "storage": "1TB", "tags": ["laptop", "apple", "pro"]}'),
('ThinkPad X1', '{"brand": "Lenovo", "cpu": "Ultra 9", "ram": 32, "storage": "512GB", "tags": ["laptop", "business"]}'),
('Mac mini', '{"brand": "Apple", "cpu": "M5", "ram": 16, "storage": "256GB", "tags": ["desktop", "apple", "compact"]}');
基本操作
値の取得
-- テキストとして取得 (->>)
SELECT name, attributes->>'brand' AS brand FROM products;
-- JSONとして取得 (->)
SELECT name, attributes->'tags' AS tags FROM products;
-- ネストした値の取得 (#>>)
SELECT attributes#>>'{tags,0}' AS first_tag FROM products;
存在チェック
-- キーの存在確認 (?)
SELECT name FROM products WHERE attributes ? 'cpu';
-- 値を含むか (@>)
SELECT name FROM products WHERE attributes @> '{"brand": "Apple"}';
-- 配列に値が含まれるか
SELECT name FROM products WHERE attributes->'tags' ? 'pro';
検索パターン
数値条件での検索
-- RAM 32GB以上の製品
SELECT name, attributes->>'ram' AS ram
FROM products
WHERE (attributes->>'ram')::int >= 32;
部分一致検索
-- brandに"App"を含む(LIKE相当)
SELECT name FROM products
WHERE attributes->>'brand' ILIKE '%app%';
配列要素での検索
-- tagsに"apple"を含む製品
SELECT name FROM products
WHERE attributes @> '{"tags": ["apple"]}';
-- 配列の長さで絞り込み
SELECT name FROM products
WHERE jsonb_array_length(attributes->'tags') >= 3;
更新操作
値の追加・更新
-- キーの追加/更新 (||)
UPDATE products
SET attributes = attributes || '{"color": "silver"}'
WHERE name = 'MacBook Pro';
-- ネストした値の更新 (jsonb_set)
UPDATE products
SET attributes = jsonb_set(attributes, '{ram}', '64')
WHERE name = 'MacBook Pro';
値の削除
-- キーの削除 (-)
UPDATE products
SET attributes = attributes - 'color'
WHERE name = 'MacBook Pro';
-- 配列から要素を削除
UPDATE products
SET attributes = jsonb_set(
attributes,
'{tags}',
(attributes->'tags') - 'compact'
)
WHERE name = 'Mac mini';
集計
JSONBの値でグループ化
-- ブランド別の製品数
SELECT attributes->>'brand' AS brand, COUNT(*)
FROM products
GROUP BY attributes->>'brand';
配列要素の展開と集計
-- タグの出現回数
SELECT tag, COUNT(*) as count
FROM products, jsonb_array_elements_text(attributes->'tags') AS tag
GROUP BY tag
ORDER BY count DESC;
インデックス設計
GINインデックス
-- JSONB全体にGINインデックス(@>, ?, ?| オペレータに有効)
CREATE INDEX idx_products_attributes ON products USING GIN (attributes);
-- 特定パスにインデックス(等値検索に有効)
CREATE INDEX idx_products_brand ON products USING BTREE ((attributes->>'brand'));
-- 数値条件のインデックス
CREATE INDEX idx_products_ram ON products USING BTREE (((attributes->>'ram')::int));
インデックスの使い分け
| 用途 | インデックス種別 | 対応オペレータ |
|---|---|---|
| 含有チェック | GIN | @>, ?, ?&, ?| |
| 等値検索 | BTREE (式) | =, <, > |
| パス検索 | GIN (jsonb_path_ops) | @> のみ(軽量) |
-- jsonb_path_opsは@>専用だがサイズが小さい
CREATE INDEX idx_products_attrs_path ON products
USING GIN (attributes jsonb_path_ops);
パフォーマンスのコツ
- 頻繁に検索する値はカラムに切り出す — JSONB内よりカラムの方が高速
- GINインデックスはWRITE負荷が高い — 更新頻度が高いテーブルでは注意
- jsonb_path_opsを優先 —
@>しか使わないなら軽量 - 大きなJSONBは分割 — 1行あたり数MBを超えるJSONBは避ける
関連記事
- MySQL JSON型の操作まとめ — MySQL側のJSON操作
- PostgreSQL JSON操作 — 基本的なJSON操作
- Laravel + Postgres JSON — LaravelからのJSON操作
- SQLで株式テクニカル指標を計算するシリーズ — SQLのウィンドウ関数活用