PostgreSQL jsonbとは何か?新人エンジニア向けにJSON保存と検索の基本をやさしく解説
こんにちは。ゆうせいです。
今回は、PostgreSQLのjsonbについて、新人エンジニア向けに解説します。
jsonbは、PostgreSQLでJSON形式のデータを保存するためのデータ型です。
JSONとは、JavaScript Object Notationの略で、データをキーと値の組み合わせで表現する形式です。
たとえば、ユーザー設定をJSONで表すと、次のようになります。
{
"theme": "dark",
"language": "ja",
"notification": true
}このデータは、「themeはdark」「languageはja」「notificationはtrue」という意味です。
高校生にもわかるようにたとえるなら、JSONはアンケート用紙のようなものです。
「名前:田中」「学年:2年」「部活:サッカー部」のように、項目名と値をセットで書く形式です。
PostgreSQLには、JSONを保存する型としてjsonとjsonbがあります。公式ドキュメントでは、jsonは入力されたテキストをそのまま保存し、処理のたびに再解析が必要になる一方、jsonbは分解されたバイナリ形式で保存されるため、入力時は少し遅くなるものの、処理時は再解析が不要で速くなりやすいと説明されています。また、jsonbはインデックスをサポートする点も大きな利点です。
jsonbを一言でいうと何か
jsonbを一言でいうと、「JSONを検索しやすい形で保存するPostgreSQLの型」です。
普通のテキストとしてJSONを保存することもできます。
でも、テキストとして保存すると、PostgreSQLは中身をJSONとして理解しにくくなります。
一方でjsonbとして保存すると、PostgreSQLはJSONのキーや値を扱いやすくなります。
| 保存方法 | イメージ | 特徴 |
|---|---|---|
| text | JSONっぽい文字列として保存する | 中身が正しいJSONかDBが保証しにくい |
| json | JSONとして保存する | 入力された形を比較的そのまま保持する |
| jsonb | JSONを検索しやすい形に変換して保存する | 検索や処理に向き、インデックスも使える |
たとえるなら、textは「メモ帳に住所をそのまま書いた状態」です。
jsonは「住所として読める形ではあるが、書いた順番や空白も残している状態」です。
jsonbは「住所を都道府県、市区町村、番地のように整理して、検索しやすい住所録にした状態」です。
jsonとjsonbの違い
jsonとjsonbは似ています。
どちらもJSONデータを保存できます。
ただし、実務ではjsonbを使う場面が多いです。
公式ドキュメントでも、ほとんどのアプリケーションでは、キー順序に依存するような特殊な事情がない限り、jsonbを選ぶのが望ましいと説明されています。jsonbは空白を保持せず、オブジェクトキーの順序も保持せず、重複キーがある場合は最後の値だけを保持します。
| 比較 | json | jsonb |
|---|---|---|
| 保存形式 | 入力テキストに近い形 | バイナリ形式に変換 |
| 空白 | 保持する | 保持しない |
| キーの順番 | 保持する | 保持しない |
| 重複キー | 保持する | 最後の値だけ残る |
| 検索性能 | 処理のたびに再解析が必要 | 再解析が不要で速くなりやすい |
| インデックス | jsonbほど扱いやすくない | GINインデックスなどを使える |
| 実務での利用 | 入力された見た目を残したい特殊用途 | 多くのアプリケーションで使いやすい |
新人エンジニアは、まずこう覚えるとよいです。
「見た目をそのまま残したいならjson、検索や実務利用を重視するならjsonb」
ただし、通常のWebアプリ開発では、jsonbを選ぶことが多いです。
jsonbを使うと何が便利なのか
jsonbの便利なところは、カラム数を増やさずに柔軟なデータを保存できる点です。
たとえば、ECサイトの商品情報を考えてみましょう。
服ならサイズや色があります。
スマホなら容量やOSがあります。
食品なら賞味期限やアレルギー情報があります。
商品ごとに必要な項目が違う場合、すべてを通常カラムにするとテーブルが複雑になります。
CREATE TABLE products (
product_id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price INTEGER NOT NULL,
attributes JSONB NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);attributesに、商品ごとの追加情報を保存できます。
INSERT INTO products (name, price, attributes)
VALUES
(
'Tシャツ',
3000,
'{"color": "black", "size": "M", "material": "cotton"}'
);
INSERT INTO products (name, price, attributes)
VALUES
(
'スマートフォン',
98000,
'{"storage": "128GB", "os": "Android", "waterproof": true}'
);同じproductsテーブルでも、商品ごとに違う属性を持てます。
学校の持ち物リストでたとえると、全員に共通する「名前」「学年」は通常カラムです。
でも、「部活の道具」「委員会の持ち物」「習い事の道具」は人によって違いますよね。
人によって違う追加情報を入れる欄がjsonbです。
jsonbの基本的な取り出し方
jsonbの中身を取り出すときは、専用の演算子を使います。
演算子とは、データに対して特定の操作をする記号です。
たとえば、足し算の+も演算子です。
jsonbでは、キーを指定して値を取り出すために、->や->>を使います。PostgreSQL公式ドキュメントでは、->はJSONオブジェクトや配列から値を取り出し、->>は値をtextとして取り出す演算子として説明されています。
| 演算子 | 意味 | 戻り値のイメージ |
|---|---|---|
| -> | JSONとして取り出す | "black" のようなJSON値 |
| ->> | 文字列として取り出す | black のようなtext |
| #> | 深い階層の値をJSONとして取り出す | ネストされたJSON |
| #>> | 深い階層の値を文字列として取り出す | text |
例を見ましょう。
SELECT
name,
attributes -> 'color' AS color_json,
attributes ->> 'color' AS color_text
FROM products;
attributes -> 'color'はJSONとして取り出します。attributes ->> 'color'は文字列として取り出します。
画面表示や条件比較で使う場合は、->>を使う場面が多いです。
jsonbの値で検索する
jsonbの中身を条件にして検索できます。
たとえば、色がblackの商品だけを探す場合です。
SELECT
product_id,
name,
price
FROM products
WHERE attributes ->> 'color' = 'black';容量が128GBの商品を探す場合です。
SELECT
product_id,
name,
price
FROM products
WHERE attributes ->> 'storage' = '128GB';waterproofがtrueの商品を探す場合です。
SELECT
product_id,
name,
price
FROM products
WHERE attributes ->> 'waterproof' = 'true';ここで注意があります。
->>で取り出した値はtextです。
trueも文字列として比較しています。
数値として比較したい場合は、型変換が必要です。
SELECT
product_id,
name,
price
FROM products
WHERE (attributes ->> 'weight')::integer < 1000;::integerは、文字列を整数に変換する書き方です。
新人エンジニアがよくつまずくポイントです。
JSONの中では数値に見えても、->>で取り出すとtextとして扱われることがあります。
計算や大小比較をするなら、型を意識してください!
jsonbの存在チェック
jsonbでは、「そのキーが存在するか」を確認できます。
たとえば、attributesにcolorというキーがある商品を探す場合です。
SELECT
product_id,
name
FROM products
WHERE attributes ? 'color';?は、指定したキーが存在するかを調べる演算子です。
これは、カバンの中に「財布」が入っているか確認するようなものです。
財布の中身までは見ていません。
まず「財布という項目があるか」を見ています。
複数キーのうち、どれかがあるかを調べる場合は?|を使います。
SELECT
product_id,
name
FROM products
WHERE attributes ?| array['color', 'size'];複数キーがすべてあるかを調べる場合は?&を使います。
SELECT
product_id,
name
FROM products
WHERE attributes ?& array['color', 'size'];jsonbの包含検索
jsonbでよく使われるのが、@>演算子です。
@>は、「左側のjsonbが右側のjsonbを含んでいるか」を調べます。
PostgreSQL公式ドキュメントでは、jsonbには包含や存在確認の機能があり、@>はあるjsonbドキュメントが別のjsonbを含むかをテストする演算子として説明されています。
たとえば、attributesにcolorがblackという情報を含む商品を探します。
SELECT
product_id,
name,
price
FROM products
WHERE attributes @> '{"color": "black"}';これは、次のような意味です。
attributesの中に、colorがblackという条件が含まれている商品を探してください。
複数条件も書けます。
SELECT
product_id,
name,
price
FROM products
WHERE attributes @> '{"color": "black", "size": "M"}';このSQLは、colorがblackで、sizeがMの商品を探します。
@>はjsonb検索でとても重要です。
特にGINインデックスと組み合わせると、検索性能を上げやすくなります。
jsonbとGINインデックス
jsonbを使う理由の1つが、GINインデックスを使えることです。
インデックスとは、検索を速くするための索引です。
本の最後にある索引を思い出してください。
「jsonb」という言葉がどのページに出ているか、索引を見ればすぐに探せますよね。
DBのインデックスも同じです。
PostgreSQL公式ドキュメントでは、GINインデックスは複合的な値の中に現れる要素を検索するようなケースのために設計されており、jsonb向けの演算子クラスも用意されています。jsonbのデフォルトGIN演算子クラスは、?、?|、?&、@>、@?、@@などをサポートします。
jsonbカラムにGINインデックスを作る例です。
CREATE INDEX idx_products_attributes
ON products
USING GIN (attributes);このインデックスがあると、次のような検索が速くなりやすいです。
SELECT
product_id,
name
FROM products
WHERE attributes @> '{"color": "black"}';ただし、インデックスを作れば何でも速くなるわけではありません。
インデックスにもコストがあります。
| メリット | デメリット |
|---|---|
| 検索が速くなりやすい | INSERTやUPDATEが少し重くなりやすい |
| 大量データで効果が出やすい | インデックス用の容量が必要 |
| @>や?系の検索と相性がよい | すべての検索に効くわけではない |
PostgreSQL公式ドキュメントでは、jsonb向けのGINインデックスにはjsonb_opsとjsonb_path_opsがあり、jsonb_path_opsはサポートする演算子が少ない一方で、対象演算子では通常より小さく高速になりやすいと説明されています。
まず新人エンジニアは、最初に通常のGINインデックスを覚えれば十分です。
CREATE INDEX idx_products_attributes
ON products
USING GIN (attributes);性能改善が必要になったときに、jsonb_path_opsや式インデックスを検討するとよいです。
よく検索するキーには式インデックスも使える
jsonb全体にGINインデックスを作る方法は柔軟です。
ただし、特定のキーだけをよく検索するなら、式インデックスが向いている場合があります。
式インデックスとは、カラムそのものではなく、式の結果に対して作るインデックスです。
たとえば、colorでよく検索する場合です。
CREATE INDEX idx_products_color
ON products ((attributes ->> 'color'));このインデックスは、attributesからcolorを取り出した結果に対して作られます。
次の検索に効きやすくなります。
SELECT
product_id,
name
FROM products
WHERE attributes ->> 'color' = 'black';PostgreSQL公式ドキュメントでも、特定キーの中身をよく問い合わせる場合は式インデックスが有効な場合があり、jsonb全体のGINインデックスより対象を絞った式インデックスのほうが小さく高速に検索できる場合があると説明されています。
たとえるなら、jsonb全体のGINインデックスは「本全体の索引」です。
式インデックスは「色だけを調べる専用索引」です。
色で検索することが多いなら、専用索引のほうが使いやすい場合があります。
jsonbを使うべき場面
jsonbは便利ですが、何でもjsonbに入れればよいわけではありません。
まず、使うべき場面を整理します。
| jsonbが向いている場面 | 理由 |
|---|---|
| 商品ごとに属性が違う | 服、食品、家電などで項目が変わるため |
| ユーザー設定を保存したい | テーマ、通知設定、表示設定などが柔軟に変わるため |
| 外部APIのレスポンスを一部保存したい | 構造が変わる可能性があるため |
| ログやイベント情報を保存したい | イベント種類によって中身が違うため |
| 拡張項目をあとから増やしたい | テーブル定義変更を減らせるため |
たとえば、ユーザー設定にはjsonbが向いています。
CREATE TABLE users (
user_id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
settings JSONB NOT NULL DEFAULT '{}'
);
INSERT INTO users (name, settings)
VALUES
(
'田中',
'{"theme": "dark", "language": "ja", "email_notification": true}'
);settingsの項目は、今後増える可能性があります。
たとえば、font_size、timezone、display_modeなどです。
こうした柔軟な設定にはjsonbが向いています。
jsonbを使わないほうがよい場面
jsonbは便利ですが、通常カラムの代わりに何でも入れるのは危険です。
特に、検索、JOIN、集計、制約が重要な項目は、普通のカラムにしたほうがよいことが多いです。
| 通常カラムにしたほうがよいもの | 理由 |
|---|---|
| 主キー | データを一意に識別するため |
| 外部キー | テーブル同士の関係を守るため |
| 名前 | よく検索や表示に使うため |
| 価格 | 数値比較や集計をよく行うため |
| 作成日時 | 並び替えや期間検索でよく使うため |
| 削除日時 | deleted_at IS NULLのように頻繁に使うため |
悪い例です。
CREATE TABLE products_bad (
data JSONB NOT NULL
);
すべてをdataに入れてしまう設計です。
{
"product_id": 1,
"name": "Tシャツ",
"price": 3000,
"deleted_at": null,
"attributes": {
"color": "black",
"size": "M"
}
}一見すると自由で便利そうです。
でも、検索、制約、集計、JOIN、インデックス設計が難しくなります。
良い例です。
CREATE TABLE products (
product_id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price INTEGER NOT NULL,
deleted_at TIMESTAMP NULL,
attributes JSONB NOT NULL DEFAULT '{}'
);よく使う項目は通常カラムにします。
商品ごとに違う追加情報だけをjsonbに入れます。
机の引き出しでたとえるなら、毎日使う筆箱やノートは机の上に置きます。
たまに使うシールや予備の文房具は引き出しに入れます。
jsonbは便利な引き出しです。
でも、毎日使うものまで全部引き出しに入れると、かえって探しにくくなります。
jsonbの設計で大切な考え方
jsonbを使うときは、「固定項目」と「可変項目」を分けることが重要です。
| 種類 | 保存場所 | 例 |
|---|---|---|
| 固定項目 | 通常カラム | id、name、price、created_at、deleted_at |
| 可変項目 | jsonb | color、size、storage、os、material |
固定項目とは、ほとんどのレコードに共通して存在し、検索や集計によく使う項目です。
可変項目とは、レコードごとに持っていたり持っていなかったりする追加情報です。
商品で考えると、nameやpriceは多くの商品に共通します。
一方で、colorやstorageやallergy_infoは商品カテゴリによって違います。
この違いを見極めることが、jsonb設計の第一歩です。
jsonbを更新する
jsonbの中身は更新できます。
ただし、通常カラムのように一部だけを簡単に書き換える感覚とは少し違います。
jsonbの一部を更新するときは、jsonb_setを使うことがあります。
UPDATE products
SET attributes = jsonb_set(attributes, '{color}', '"white"')
WHERE product_id = 1;このSQLは、attributesのcolorをwhiteに変更します。
注意点として、JSONの文字列はダブルクォート付きのJSON値として渡す必要があります。
'"white"'という書き方が少し不思議に見えますよね。
外側のシングルクォートはSQL文字列です。
内側のダブルクォートはJSONの文字列です。
お弁当箱でたとえるなら、外側の箱がSQL、内側のおかずがJSONです。
SQLの文字列として、JSONの文字列を入れているため、二重に見えるのです。
jsonbのNULLに注意する
jsonbでは、SQLのNULLとJSONのnullを区別する必要があります。
PostgreSQL公式ドキュメントでも、JSONのnullはSQLのNULLとは別の概念だと説明されています。
例を見ましょう。
{
"nickname": null
}
このnicknameは、JSONの中にキーは存在しています。
ただし、値がnullです。
一方で、次のJSONにはnicknameキー自体がありません。
{
"theme": "dark"
}
この2つは意味が違います。
| 状態 | 意味 |
|---|---|
| "nickname": null | nicknameという項目はあるが、値がない |
| nicknameキーが存在しない | nicknameという項目自体がない |
| SQLのNULL | カラム全体がNULL |
新人エンジニアは、ここを混同しやすいです。
「空のロッカーがある」のと「ロッカーそのものがない」のは違いますよね。
JSONのnullとキーなしも同じです。
jsonbを使うメリット
jsonbのメリットを整理します。
| メリット | 内容 |
|---|---|
| 柔軟なデータを保存できる | 項目が変わりやすいデータに対応しやすい |
| JSONとして正しいか確認される | 不正なJSONを保存しにくい |
| 検索用の演算子がある | ->、->>、@>、?などを使える |
| GINインデックスを使える | 大量データでも検索を速くしやすい |
| 外部APIデータと相性がよい | JSON形式のレスポンスを扱いやすい |
jsonbは、変化しやすいデータを扱うときに便利です。
特に、商品属性、ユーザー設定、イベントログ、APIレスポンスの保存で役立ちます。
jsonbを使うデメリット
jsonbにはデメリットもあります。
| デメリット | 内容 |
|---|---|
| 設計が雑になりやすい | 何でもjsonbに入れてしまう危険がある |
| 制約を付けにくい | 通常カラムよりNOT NULLや外部キーを扱いにくい |
| 型が見えにくい | JSON内の値が文字列なのか数値なのか意識が必要 |
| 検索SQLが読みにくくなる | ->>や@>が増えると初心者には難しくなる |
| 更新が複雑になりやすい | jsonb_setなど専用関数が必要になることがある |
jsonbは、自由に書けるノートのようなものです。
自由だから便利です。
でも、ルールなく書くと、あとから誰も読めないノートになります。
DB設計でも同じです。
jsonbを使うなら、「何をjsonbに入れるのか」「何は通常カラムにするのか」を決めてから使いましょう。
Javaアプリからjsonbを扱うときの考え方
新人エンジニアがJavaやSpring BootでPostgreSQLを使う場合、jsonbカラムは文字列として渡すこともあります。
たとえば、JDBCでINSERTするイメージです。
String sql = """
INSERT INTO products (name, price, attributes)
VALUES (?, ?, ?::jsonb)
""";
try (Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, "Tシャツ");
statement.setInt(2, 3000);
statement.setString(3, "{\"color\":\"black\",\"size\":\"M\"}");
statement.executeUpdate();
}?::jsonbとしている理由は、渡した文字列をjsonbとして扱ってほしいからです。
ただし、実務ではJSON文字列を手作業で連結しないほうが安全です。
ダブルクォートのエスケープミスや、不正なJSONを作る可能性があるためです。
Javaでは、JacksonなどのJSONライブラリでオブジェクトをJSON文字列に変換してから渡すほうが安全です。
手でJSONを書くのは、封筒に宛名を手書きするようなものです。
少量なら問題ありません。
でも、たくさん送るなら印刷やテンプレートを使ったほうがミスが減ります。
jsonbを使うときの実務チェックリスト
jsonbを使う前に、次のチェックをしてください。
| チェック | 確認すること |
|---|---|
| 1 | その項目は本当に可変項目か |
| 2 | よく検索する項目をjsonbに入れていないか |
| 3 | 外部キーやNOT NULL制約が必要な項目ではないか |
| 4 | JSON内の型が文字列、数値、真偽値のどれか決まっているか |
| 5 | 検索に必要なインデックスを検討したか |
| 6 | 古いJSON構造と新しいJSON構造が混ざる可能性を考えたか |
| 7 | アプリ側でJSON生成を安全に行っているか |
特に大事なのは、1番と2番です。
本当に可変項目ならjsonbは便利です。
でも、毎回検索する重要項目なら通常カラムを検討してください。
便利だからjsonbに入れるのではありません。
データの性質に合っているからjsonbを使うのです。
まとめ
PostgreSQLのjsonbは、JSON形式のデータを検索しやすい形で保存するためのデータ型です。
jsonとjsonbの大きな違いは保存形式と効率です。jsonは入力テキストに近い形を保持しますが、処理のたびに再解析が必要です。jsonbはバイナリ形式に変換して保存するため、入力時に少しコストはありますが、処理が速くなりやすく、インデックスも使えます。
| ポイント | 内容 |
|---|---|
| jsonbの正体 | JSONを検索しやすい形で保存するPostgreSQLの型 |
| 向いているデータ | 商品属性、ユーザー設定、イベントログ、APIレスポンス |
| 基本演算子 | ->、->>、?、?|、?&、@> |
| 性能改善 | GINインデックスや式インデックスを検討する |
| 注意点 | 何でもjsonbに入れず、固定項目は通常カラムにする |
一言でまとめるなら、jsonbは「形が変わりやすい追加情報を、JSONのまま保存して検索できる便利な引き出し」です。
ただし、引き出しに何でも詰め込むと、あとから探せなくなります。
id、name、price、created_at、deleted_atのような重要でよく使う項目は通常カラムにしましょう。
商品ごとに違う属性、ユーザーごとの設定、イベントごとの追加情報のような可変項目にjsonbを使うと、設計がきれいになりやすいです。
今後の学習では、まずJSONの基本、PostgreSQLのjsonb演算子、GINインデックス、式インデックス、Javaからのjsonb登録、DB設計における正規化との使い分けを順番に学ぶとよいです。まずは小さなproductsテーブルを作り、attributes JSONBに色やサイズを入れて、->>と@>で検索する練習から始めてください!
セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。
投稿者プロフィール


