一般社団法人 全国個人事業主支援協会

COLUMN コラム

  • PostgreSQLのJSONB活用術:リレーショナルとドキュメントの融合

なぜPostgreSQLでJSONBを使うのか

PostgreSQLのJSONB型は、リレーショナルデータベースの堅牢さとドキュメントストアの柔軟性を兼ね備えた強力な機能です。MongoDBのようなNoSQLデータベースを別途運用するコストを考えると、PostgreSQL一本でリレーショナルデータとドキュメントデータの両方を扱えるメリットは非常に大きいです。

筆者が過去に担当したECサイトのプロジェクトでは、商品のカスタム属性管理にJSONBを導入したことで、スキーマ変更なしに多様な商品カテゴリに対応できるようになりました。本記事では、実務で役立つ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データの部分更新には 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データを使った集約処理も強力です。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';

インデックス戦略:GINインデックスの活用

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クエリのみをサポートする。用途に応じて使い分けることが重要です。

JSONBを使うべき場面と避けるべき場面

JSONBが適している場面

  • スキーマレスなデータ:商品属性、ユーザー設定、フォームデータなど、構造が頻繁に変わるデータ
  • 外部APIのレスポンス保存:サードパーティAPIのレスポンスをそのまま保存したい場合
  • イベントログ:構造が異なるイベントデータを柔軟に格納したい場合
  • メタデータ:本体のリレーショナルデータに付随する補助的な情報

JSONBを避けるべき場面

  • 頻繁にJOINするデータ:JOINの対象となるデータは正規化してカラムに持つべき
  • 一意制約やFK制約が必要なデータ:JSONB内部のフィールドに対して、データベースレベルの制約を適用するのは困難
  • 大量の数値集計が必要なデータ:JSONBからの値抽出はカラム直接アクセスより遅いため、集計対象のデータは正規化すべき

まとめ

PostgreSQLのJSONBは、リレーショナルモデルの堅牢さを保ちながら、柔軟なデータモデリングを実現する強力なツールです。ただし万能ではなく、正規化すべきデータとJSONBに格納すべきデータの見極めが重要になります。筆者の経験則としては、「クエリの条件やJOINの対象になるデータは正規化、付随的な属性やスキーマが不定なデータはJSONB」という方針がうまく機能する。適材適所で使い分けることで、PostgreSQLのポテンシャルを最大限に引き出せるでしょう。

この記事をシェアする

  • Twitterでシェア
  • Facebookでシェア
  • LINEでシェア