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のキーや値を扱いやすくなります。

保存方法イメージ特徴
textJSONっぽい文字列として保存する中身が正しいJSONかDBが保証しにくい
jsonJSONとして保存する入力された形を比較的そのまま保持する
jsonbJSONを検索しやすい形に変換して保存する検索や処理に向き、インデックスも使える

たとえるなら、textは「メモ帳に住所をそのまま書いた状態」です。

jsonは「住所として読める形ではあるが、書いた順番や空白も残している状態」です。

jsonbは「住所を都道府県、市区町村、番地のように整理して、検索しやすい住所録にした状態」です。

jsonとjsonbの違い

jsonとjsonbは似ています。

どちらもJSONデータを保存できます。

ただし、実務ではjsonbを使う場面が多いです。

公式ドキュメントでも、ほとんどのアプリケーションでは、キー順序に依存するような特殊な事情がない限り、jsonbを選ぶのが望ましいと説明されています。jsonbは空白を保持せず、オブジェクトキーの順序も保持せず、重複キーがある場合は最後の値だけを保持します。

比較jsonjsonb
保存形式入力テキストに近い形バイナリ形式に変換
空白保持する保持しない
キーの順番保持する保持しない
重複キー保持する最後の値だけ残る
検索性能処理のたびに再解析が必要再解析が不要で速くなりやすい
インデックス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
可変項目jsonbcolor、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": nullnicknameという項目はあるが、値がない
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制約が必要な項目ではないか
4JSON内の型が文字列、数値、真偽値のどれか決まっているか
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に色やサイズを入れて、->>と@>で検索する練習から始めてください!

セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。

学生時代は趣味と実益を兼ねてリゾートバイトにいそしむ。長野県白馬村に始まり、志賀高原でのスキーインストラクター、沖縄石垣島、北海道トマム。高じてオーストラリアのゴールドコーストでツアーガイドなど。現在は野菜作りにはまっている。