PostgreSQLは、json型とjsonb型により、JSONデータの保存とクエリを強力にサポートしています。特にjsonb型はデータをバイナリ形式で保存するため、テキストベースのjson型と比べて、より効率的なインデックス作成とクエリが可能です。
このチュートリアルでは、PostgreSQLでよく使われるJSON操作を、アプリケーションでそのまま使える実践的な例とともに紹介します。
JSONデータの作成
まず、jsonbカラムを持つテーブルを作成し、サンプルデータを挿入しましょう。
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
info JSONB
);
-- Insert a customer with basic information
INSERT INTO customers (info) VALUES (
'{"name": "Alice Smith", "email": "alice@example.com", "age": 32, "addresses": [
{"type": "home", "city": "San Francisco", "state": "CA"},
{"type": "work", "city": "Oakland", "state": "CA"}
]}'
);
-- Insert another customer
INSERT INTO customers (info) VALUES (
'{"name": "Bob Johnson", "email": "bob@example.com", "age": 45, "addresses": [
{"type": "home", "city": "Seattle", "state": "WA"}
], "orders": [101, 102, 103]}'
);
基本的なJSON操作
JSONドキュメント全体の取得
SELECT info FROM customers WHERE id = 1;
結果:
{
"age": 32,
"name": "Alice Smith",
"email": "alice@example.com",
"addresses": [
{"city": "San Francisco", "type": "home", "state": "CA"},
{"city": "Oakland", "type": "work", "state": "CA"}
]
}
JSONから値を抽出する
PostgreSQLには、JSONから値を抽出するための演算子が複数用意されています。
-> 演算子で特定のフィールドを抽出する(JSONを返す)
-- Get the name field as JSON
SELECT info -> 'name' FROM customers;
結果:
"Alice Smith"
"Bob Johnson"
-» 演算子で特定のフィールドを抽出する(テキストを返す)
-- Get the name field as text
SELECT info ->> 'name' AS customer_name FROM customers;
結果:
customer_name
----------------
Alice Smith
Bob Johnson
ネストされたオブジェクトへのアクセス
-- Getting the first address (array element 0) city
SELECT info -> 'addresses' -> 0 ->> 'city' AS home_city FROM customers;
結果:
home_city
------------
San Francisco
Seattle
#> 演算子でパスを指定してアクセスする
-- Using the path syntax to get the same value
SELECT info #> '{addresses,0,city}' FROM customers;
結果:
"San Francisco"
"Seattle"
#» 演算子でパスを指定してテキストを取得する
-- Using the path syntax to get the same value as text
SELECT info #>> '{addresses,0,city}' AS home_city FROM customers;
結果:
home_city
------------
San Francisco
Seattle
JSONデータの変更
PostgreSQLには、JSONデータを変更するための関数がいくつか用意されています。
jsonb_setでフィールドを追加する
-- Add a new field 'premium_customer' to a customer
UPDATE customers
SET info = jsonb_set(info, '{premium_customer}', 'true')
WHERE id = 1;
- 演算子でフィールドを削除する
-- Remove the premium_customer field
UPDATE customers
SET info = info - 'premium_customer'
WHERE id = 1;
|| でJSONオブジェクトをマージする
-- Add multiple fields by merging objects
UPDATE customers
SET info = info || '{"status": "active", "last_login": "2025-03-23"}'::jsonb
WHERE id = 1;
ネストされたフィールドの更新
-- Update a nested field (home address state)
UPDATE customers
SET info = jsonb_set(
info,
'{addresses,0,state}',
'"NY"'::jsonb
)
WHERE id = 1;
JSON配列のクエリ
JSON配列の操作はよく行われる処理です。
配列の長さを取得する
-- Get the number of addresses for each customer
SELECT
info ->> 'name' AS customer_name,
jsonb_array_length(info -> 'addresses') AS address_count
FROM customers;
結果:
customer_name | address_count
---------------+--------------
Alice Smith | 2
Bob Johnson | 1
特定の配列要素を抽出する
-- Get the second address (index 1) for Alice
SELECT info -> 'addresses' -> 1 FROM customers WHERE id = 1;
結果:
{"city": "Oakland", "type": "work", "state": "CA"}
配列を展開して個々の要素をクエリする
-- Unnest the addresses array to query individual addresses
SELECT
c.id,
c.info ->> 'name' AS customer_name,
jsonb_array_elements(c.info -> 'addresses') AS address
FROM customers c;
結果:
id | customer_name | address
---+---------------+------------------------------------------------------
1 | Alice Smith | {"city": "San Francisco", "type": "home", "state": "NY"}
1 | Alice Smith | {"city": "Oakland", "type": "work", "state": "CA"}
2 | Bob Johnson | {"city": "Seattle", "type": "home", "state": "WA"}
展開した配列でフィルタリングする
-- Find customers with addresses in CA
SELECT DISTINCT
c.id,
c.info ->> 'name' AS customer_name
FROM customers c,
jsonb_array_elements(c.info -> 'addresses') AS addr
WHERE addr ->> 'state' = 'CA';
結果:
id | customer_name
---+--------------
1 | Alice Smith
キーと値の存在チェック
キーが存在するか確認する
-- Find customers who have the 'orders' key
SELECT id, info ->> 'name' AS customer_name
FROM customers
WHERE info ? 'orders';
結果:
id | customer_name
---+--------------
2 | Bob Johnson
配列内の値を確認する
-- Find customers with a specific order number
SELECT id, info ->> 'name' AS customer_name
FROM customers
WHERE info -> 'orders' ? '101';
結果:
id | customer_name
---+--------------
2 | Bob Johnson
いずれかの配列要素が一致するか確認する
-- Find customers who have an address in Oakland (using path operators)
SELECT id, info ->> 'name' AS customer_name
FROM customers
WHERE info @> '{"addresses": [{"city": "Oakland"}]}';
結果:
id | customer_name
---+--------------
1 | Alice Smith
JSONデータのインデックス作成
大規模なJSONデータセットを扱う本番データベースでは、適切なインデックス作成が不可欠です。
包含クエリ用のGINインデックスを作成する
-- Create a GIN index for faster containment operations
CREATE INDEX idx_customers_info ON customers USING GIN (info);
JSONフィールド専用のインデックスを作成する
-- Create an index on the 'email' field for fast lookups
CREATE INDEX idx_customers_email ON customers ((info ->> 'email'));
クエリでインデックス付きフィールドを使う
-- This query can use the index we created above
SELECT id, info ->> 'name' AS customer_name
FROM customers
WHERE info ->> 'email' = 'alice@example.com';
JSONとテーブルの相互変換
JSONを行に変換する(jsonb_to_record)
-- Extract JSON fields into a tabular structure
SELECT
id,
rec.name,
rec.email,
rec.age
FROM
customers,
jsonb_to_record(info) AS rec(
name text,
email text,
age int
);
結果:
id | name | email | age
---+--------------+-------------------+-----
1 | Alice Smith | alice@example.com | 32
2 | Bob Johnson | bob@example.com | 45
ネストされた配列をテーブルに変換する(jsonb_to_recordset)
-- Extract addresses from all customers into a table
SELECT
c.id,
c.info ->> 'name' AS customer_name,
addr.type AS address_type,
addr.city,
addr.state
FROM
customers c,
jsonb_to_recordset(c.info -> 'addresses') AS addr(
type text,
city text,
state text
);
結果:
id | customer_name | address_type | city | state
---+---------------+--------------+---------------+------
1 | Alice Smith | home | San Francisco | NY
1 | Alice Smith | work | Oakland | CA
2 | Bob Johnson | home | Seattle | WA
クエリ結果をJSONに変換する
-- Aggregate customer orders into a JSON array
SELECT
c.id,
c.info ->> 'name' AS customer_name,
json_agg(o.order_id) AS orders
FROM
customers c
CROSS JOIN LATERAL (
SELECT jsonb_array_elements_text(c.info -> 'orders') AS order_id
) o
GROUP BY c.id, c.info ->> 'name';
結果:
id | customer_name | orders
---+---------------+----------
2 | Bob Johnson | ["101", "102", "103"]
パフォーマンスに関する考慮事項
PostgreSQLでJSONを扱う際は、次のパフォーマンス上の考慮事項に注意してください。
-
jsonではなくjsonbを使う: ほとんどの操作でjsonb型の方が高いパフォーマンスを発揮します。 -
戦略的にインデックスを作成する: 包含クエリにはGINインデックスを使い、頻繁にクエリするフィールドには専用のインデックスを作成しましょう。
-
非正規化は慎重に検討する: JSONを使えばデータモデルを非正規化できますが、過度なネストはクエリのパフォーマンスに影響します。
-
クエリパターンに注意する: 複雑なJSONパス式や、大きな配列の頻繁な展開はコストの高い操作になり得ます。
-
配列のサイズを制限する: 非常に大きなJSON配列は、展開時にパフォーマンス問題を引き起こす可能性があります。
-
必要なフィールドだけを取得する: JSONドキュメント全体を返すのではなく、必要なフィールドだけを抽出しましょう。
まとめ
PostgreSQLのJSON機能は、半構造化データを保存・クエリする必要があるアプリケーションにとって優れた選択肢です。このチュートリアルで紹介した演算子と関数を使えば、成熟したリレーショナルデータベースの性能と信頼性を活かしながら、JSONデータを効果的に扱えます。
複雑なデータモデルでは、ハイブリッドなアプローチを検討してください。固定スキーマや頻繁にクエリするデータには構造化されたテーブルを使い、柔軟でスキーマレスなデータや、リレーショナルにモデル化しにくいネスト構造にはJSONを使います。
これらの例を実際に試してみませんか?Beekeeper Studioを開いて、今すぐPostgreSQLのJSON機能を体験してみましょう。
Beekeeper Studioは無料のオープンソースのデータベースGUIです
これまで使った中で最高のSQLクエリ&エディタツールです。データベースの管理に必要なものがすべて揃っています。 - ⭐⭐⭐⭐⭐ Mit
Beekeeper Studioは高速で直感的、そして使いやすいツールです。多数のデータベースに対応し、Windows、Mac、Linuxで快適に動作します。
Beekeeper Studio 利用者の声
「Beekeeper Studioは、これまでのSQLワークフローを完全に置き換えてくれました。高速で直感的、データベース作業がまた楽しくなりました。」
「多くのデータベースGUIを試してきましたが、Beekeeperは機能とシンプルさのバランスが絶妙です。とにかくちゃんと動きます。」