PostgreSQLのJSONB型は、リレーショナルデータベースの堅牢さとドキュメントストアの柔軟性を兼ね備えた強力な機能です。MongoDBのようなNoSQLデータベースを別途運用するコストを考えると、PostgreSQL一本でリレーショナルデータとドキュメントデータの両方を扱えるメリットは非常に大きいです。
筆者が過去に担当したECサイトのプロジェクトでは、商品のカスタム属性管理にJSONBを導入したことで、スキーマ変更なしに多様な商品カテゴリに対応できるようになりました。本記事では、実務で役立つJSONB活用のパターンとベストプラクティスを解説します。
まず、JSONBを使ったテーブル定義と基本的なCRUD操作を見ていこう。
-- 商品テーブルの定義
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
base_price NUMERIC(10,2) NOT NULL,
attributes JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW()
);
-- データの挿入
INSERT INTO products (name, base_price, attributes) VALUES
('ノートPC', 89800, '{
"brand": "TechCorp",
"specs": {
"cpu": "Core i7-13700",
"ram_gb": 16,
"storage_gb": 512,
"display_inch": 15.6
},
"colors": ["シルバー", "ブラック"],
"wireless": true
}'),
('ワイヤレスイヤホン', 15800, '{
"brand": "AudioMax",
"specs": {
"driver_mm": 10,
"battery_hours": 8,
"noise_canceling": true
},
"colors": ["ホワイト", "ブラック", "ネイビー"]
}');
JSONB型のカラムには、構造が異なるJSONデータを自由に格納できます。上記の例では、PCとイヤホンで異なる属性を持つが、同一テーブルに格納しています。
JSONBデータへのアクセスには、->と->>の2種類の演算子を使い分ける。->はJSONBオブジェクトとして返し、->>はテキストとして返す。
-- ブランド名をテキストで取得
SELECT name, attributes->>'brand' AS brand
FROM products;
-- ネストしたキーにアクセス(CPU情報)
SELECT name, attributes->'specs'->>'cpu' AS cpu
FROM products
WHERE attributes->'specs' ? 'cpu';
-- JSONB内の数値で絞り込み
SELECT name, base_price
FROM products
WHERE (attributes->'specs'->>'ram_gb')::int >= 16;
-- 配列内の要素を検索
SELECT name
FROM products
WHERE attributes->'colors' @> '"ブラック"';
JSONBデータの部分更新には jsonb_set 関数を使う。全体を置換する必要がないため、パフォーマンスの面でも効率的です。
-- 特定のキーの値を更新
UPDATE products
SET attributes = jsonb_set(attributes, '{specs,ram_gb}', '32')
WHERE name = 'ノートPC';
-- 新しいキーを追加
UPDATE products
SET attributes = attributes || '{"warranty_years": 3}'
WHERE name = 'ノートPC';
-- キーの削除
UPDATE products
SET attributes = attributes - 'wireless'
WHERE name = 'ノートPC';
JSONBデータを使った集約処理も強力です。jsonb_array_elements 関数で配列を展開し、通常のSQLと同様に集約できます。
-- カラーバリエーション数で集計
SELECT name,
jsonb_array_length(attributes->'colors') AS color_count
FROM products
ORDER BY color_count DESC;
-- ブランド別の平均価格
SELECT attributes->>'brand' AS brand,
AVG(base_price) AS avg_price,
COUNT(*) AS product_count
FROM products
GROUP BY attributes->>'brand';
JSONBカラムに対する検索パフォーマンスを向上させるには、GIN(Generalized Inverted Index)インデックスが不可欠です。
-- JSONB全体に対するGINインデックス
CREATE INDEX idx_products_attributes
ON products USING GIN (attributes);
-- 特定のパスに対する式インデックス
CREATE INDEX idx_products_brand
ON products USING BTREE ((attributes->>'brand'));
-- jsonb_path_ops演算子クラスの使用(@>演算子専用、サイズが小さい)
CREATE INDEX idx_products_attributes_pathops
ON products USING GIN (attributes jsonb_path_ops);
jsonb_path_ops を指定したGINインデックスは、デフォルトの jsonb_ops より小さく高速だが、@> 演算子でのcontainmentクエリのみをサポートする。用途に応じて使い分けることが重要です。
PostgreSQLのJSONBは、リレーショナルモデルの堅牢さを保ちながら、柔軟なデータモデリングを実現する強力なツールです。ただし万能ではなく、正規化すべきデータとJSONBに格納すべきデータの見極めが重要になります。筆者の経験則としては、「クエリの条件やJOINの対象になるデータは正規化、付随的な属性やスキーマが不定なデータはJSONB」という方針がうまく機能する。適材適所で使い分けることで、PostgreSQLのポテンシャルを最大限に引き出せるでしょう。