---
title: "PostgreSQL JSONBの使い方｜json型との格納差・GIN索引と正規化列の設計境界"
url: "https://www.issoh.co.jp/tech/details/16990/"
published: 2026-08-26
updated: 2026-08-26
categories: ["データベース"]
publisher: "株式会社一創"
---

# PostgreSQL JSONBの使い方｜json型との格納差・GIN索引と正規化列の設計境界

PostgreSQLでJSONを列に入れるとき、型の選択肢は`json`と`jsonb`の2つ。名前は一文字違いですが、格納形式が別物で、索引を張れるかどうかも分かれます。この記事では格納差から入り、取り出しと判定の演算子、GIN索引のオペレータクラスの選び方、項目を正規化列へ出すかJSONB列に残すかの境界までを、18系（2026年8月時点の最新マイナーは18.6）の仕様に沿って整理しました。全般の位置づけは[データベースとは？種類・DBMS・RDBとNoSQLの選び方](https://www.issoh.co.jp/tech/details/13012/)に譲ります。

## まとめ：PostgreSQL JSONBの採用可否と索引設計で先に決める4点

結論を4点にまとめます。第一に、型の選択は実質`jsonb`の一択です。`json`が勝つのは、空白・キーの順序・重複キーをそのまま残したい監査目的の保管だけで、索引を張れるのは`jsonb`側に限られます。

第二に、演算子は戻り値の型で選ぶ。矢印1本はjsonbを返し、矢印2本はtextを返すため、比較や集計に回すならキャストが要ります。包含と存在の判定はトップレベルにしか効きません。入れ子の中まで見ると誤解して条件を書くと、静かに0件が返ります。

第三に、GIN索引は張る前にオペレータクラスを決めます。既定の`jsonb_ops`はキー存在の3演算子まで拾い、`jsonb_path_ops`は包含とjsonpath一致に絞る代わりに索引がかなり小さくなる。値の大小比較や並べ替えはどちらでも拾えず、そこは式インデックスの担当です。

第四に、正規化列とJSONB列の境界は「検索・集計・制約の対象になるか」で引く。案件ごとに増える付帯情報はJSONBに残し、絞り込みや突合の軸になる項目は列へ出します。全部JSONBへ入れると、行全体が書き換わる更新コストと、キー単位の統計を持たないことによる実行計画のずれが効いてきます。

## json型とjsonb型の格納差から決まる再解析コストと索引の可否

ふたつの型は、受け取ったJSONをどこに置くかが違います。そこから処理速度も索引の可否も派生します。

### json型が保持しjsonb型が捨てる空白・キー順・重複キーの3要素

`json`は入力テキストをそのまま複製して保存し、関数を呼ぶたびに解析し直します。処理側にコストが寄る設計です。対する`jsonb`は分解済みのバイナリ形式で持ち、入力時に変換のオーバーヘッドが乗る代わり、以後の処理では再解析が起きません。

| 観点        | json        | jsonb             |
| --------- | ----------- | ----------------- |
| 格納形式      | 入力テキストの複製   | 分解済みバイナリ          |
| 意味を持たない空白 | そのまま保持      | 保持しない             |
| キーの順序     | 入力順を保持      | 保持しない             |
| 重複キー      | すべて残る       | 最後の値だけ残る          |
| 処理時の再解析   | 関数呼び出しごとに発生 | 不要                |
| 索引        | 式インデックスのみ   | GIN・btree・hashが可能 |

実務で効くのは重複キーの扱いです。外部APIのレスポンスを流し込む処理には、同じキーが2回現れる壊れたペイロードが混ざる。`jsonb`へ入れた時点で最後の値だけが残り、壊れていた事実は消えます。取り込みログを別に残さないなら、原文の保管先を分けます。

### jsonbが拒否する数値範囲とヌル文字｜格納時に落ちる入力の条件

JSONのプリミティブ型は、PostgreSQL側の型に対応づけられています。文字列はtext、数値はnumeric、真偽値はboolean。JSONのnullはSQLのNULLとは別概念で、この対応関係がそのまま入力の制約になります。

制約は4つ。文字列ではヌル文字が禁止で、データベースのエンコーディングに無い文字を指すUnicodeエスケープも通りません。数値ではNaNと無限大が使えず、真偽値は小文字の`true`と`false`だけが受理されます。そして`jsonb`だけは、numericの範囲を超える数値を拒否する。`json`は素通しするため、テキストで保管していた列を`jsonb`へ移行した途端に取り込みが落ちます。

移行前に確認するのは、指数表記の巨大な数値と、外部システムが送るID文字列に紛れたヌル文字の2つ。なお18では`jsonb`のnullをスカラー型へキャストするとNULLが返ります。17以前はエラーだったため、そこで書いた例外処理は移行後に不要な分岐として残ります。

### json型を選ぶ数少ない条件と18でのSIMD高速化が変えない前提

18では長いJSON文字列の処理にSIMDが使われるようになり、解析そのものは速くなりました。ただし`json`と`jsonb`の優劣を入れ替える変更ではない。索引を張れないという`json`の性質は残ったままだからです。

`json`を選ぶ条件は狭い。原文を一字一句そのまま保全する監査要件があるか、キーの順序に意味がある外部仕様へ再送するか、書き込むだけで検索に使わない生ログか。いずれにも当てはまらなければ`jsonb`です。他のRDBMSから移ってきた場合の対応関係は[PostgreSQLとMySQLでデータ型と全文検索がどう違うか](https://www.issoh.co.jp/tech/details/3545/)に整理しました。なお、どちらの型もJSON文書としての妥当性しか検査しません。「この列にはuser\_idが必ず入る」といった構造の保証がほしいなら、[JSON Schemaで構造を検証する書き方](https://www.issoh.co.jp/tech/details/16289/)をアプリケーション側に置くか、後述の生成列と制約で受けます。

## 戻り値の型と判定範囲で使い分けるjsonbの取り出し演算子と更新関数

演算子は数が多く見えますが、軸は「何を返すか」と「どこまで見るか」の2つです。この2軸で並べ直します。

### テキスト取り出しとjsonb取り出しの戻り値の差で決まるキャスト

取り出し系は4つ。矢印1本はキーや添字を指定してjsonbを返し、矢印2本は同じ指定でtextを返します。井桁付きの2つはパスを配列で受け取る版で、型の関係は矢印と同じです。

取り違えは2つ。ひとつは、jsonbを返す演算子の結果をWHERE句で文字列と比較するケースで、jsonbの文字列はクォートを含む表現のためtextとは一致しません。もうひとつは、数値として比べたいのに矢印2本のtextのまま大小を比較するケース。文字列順に並ぶため「9」が「10」より大きく扱われます。数値なら`numeric`へキャストする。存在しないキーを指すとNULLが伝播し、条件が静かに偽になります。

### 添字記法が使える14以降の書き換えと配列スライス非対応の制約

14から、`jsonb`は添字記法で読み書きできます。入れ子は添字を連ね、配列の添字は0始まり、負の整数は末尾から数える。結果は常にjsonb型です。

制限は2つ。スライス式は使えません。戻り値がjsonb固定なので、テキストとして取り出す場面では矢印2本のほうが短く書けます。`UPDATE`のSET句で添字を左辺に置ける点は効きますが、既存コードを一括置換する価値があるほどの差ではない。なお14は2026年11月12日にEOLを迎えます。この記法を前提にできるかより先に、稼働バージョンの更新計画を確認する順番です。

### 包含判定と存在判定がトップレベルに限られることで起きる誤判定

判定系の中心は包含演算子と存在演算子です。包含は、右側の文書が左側に構造とデータの両面で含まれるかを見る。配列要素の順序は無視され、重複要素は1回として扱われます。例外がひとつあり、配列はその中のスカラー値を含むと判定されますが、逆向きには成り立ちません。

誤判定が出るのは存在演算子のほうです。トップレベルのオブジェクトキーまたは配列要素としか一致せず、値の側は見ません。キーがfooで値がbarのオブジェクトにbarの存在を問えば偽が返る。fooの中にbarを持つ二段構造でも偽です。複数キーをまとめて見る2つの派生演算子も同じ制約を引き継ぐため、入れ子の中を探すならパス指定の包含条件かjsonpathへ切り替えます。

### 部分更新に見える書き換えの実体とjsonb\_setで潰れるキーの扱い

書き換え系は用途で分かれます。

- `jsonb_set`：既存パスの値を置き換える。パスが無いときの挙動は引数で制御
- `jsonb_insert`：指定位置に要素を挿入する。既存値は上書きしない
- 連結演算子：2つのjsonbを結合する。同じキーは右側で上書き
- 減算演算子：キーや配列要素を削除する。井桁付きの版はパス指定
- `jsonb_strip_nulls`：null値のキーを除く。18で配列のnull要素も対象にできる

注意したいのは連結演算子の上書き規則です。同名キーが右側で常に勝つため、部分マージのつもりで入れ子を丸ごと差し替える事故が起きる。一部だけ変えたいなら`jsonb_set`でパスを指定します。どれを使っても更新の実体は行の書き換えです。

## jsonpathと17のSQL/JSON関数で書く検索条件と表形式への展開

入れ子の奥を条件にしたい、あるいはJSONの配列を行に開きたい。この2つは演算子では届かず、jsonpathとSQL/JSON関数の担当です。

### jsonpath一致演算子の2種の違いと述語を書く位置の使い分け

jsonpathを使う演算子は2つ。一方はパス式が1つ以上の項目を返すかを真偽で返し、もう一方はパス式そのものを述語として評価した結果を返します。目安は、述語をパスの中に括弧で書くなら前者、パス全体が比較式なら後者です。

関数版も対応しています。`jsonb_path_exists`は真偽を返し、`jsonb_path_query`は一致した項目を集合として返す。1件だけ欲しい場面で後者を使うと複数行が返って結合が膨らむため、単一値には`JSON_VALUE`か矢印演算子を使います。jsonpathの利点は、配列の任意要素に対する条件を1つの式で書けることに尽きます。

### 17で入ったキャストメソッドで型をそろえてから比較する書き方

17では、JSON値を他のJSONデータ型へ変換するjsonpathメソッドが追加されました。`.bigint()`、`.integer()`、`.number()`、`.decimal()`、`.boolean()`、`.string()`、`.date()`と時刻・日時系を合わせた11種です。

効くのは、外部システムから来たJSONで数値が文字列として入っている場合。`.number()`や`.decimal()`を挟んで比較の前に型をそろえます。ただし索引が効かなくなるため、応急処置です。混在が恒常的なら、取り込み処理で正規化するかスキーマ検証で弾く側へ寄せる。吸収し続けると遅いクエリが固定化します。

### 17で入ったJSON\_TABLEで配列を行に開く書き方と適用の限界

17では`JSON_TABLE`も入り、JSON文書をテーブル表現へ変換してSELECTのFROM句にタプル源として置けるようになりました。`JSON_EXISTS`・`JSON_QUERY`・`JSON_VALUE`の問い合わせ関数と、`JSON`・`JSON_SCALAR`・`JSON_SERIALIZE`の構成関数も同時に追加されています。

使いどころは、JSON配列を行へ展開して既存テーブルと結合する集計処理です。従来`jsonb_array_elements`と`jsonb_to_record`を組み合わせていた処理が、1つの構文にまとまる。ただし16以前では使えません。16系のまま動かす環境が残るなら、従来の関数で書くかバージョンを上げてから移ります。確認先は[Amazon RDS for PostgreSQLの対応バージョンと拡張機能の統制](https://www.issoh.co.jp/tech/details/16972/)にまとめました。

## GIN索引のオペレータクラス選択とjsonbが索引で拾えない検索条件

JSONBに索引を張る話は、ほぼGIN索引の話です。最初に決めるのはオペレータクラスで、後から変えるには索引を作り直します。

### 既定のjsonb\_opsと小さいjsonb\_path\_opsの対応演算子とサイズ差

GINには`jsonb`用のオペレータクラスが2つ。既定の`jsonb_ops`はキーと値のそれぞれに独立した索引項目を作り、`jsonb_path_ops`は値ごとにだけ索引項目を作ります。この構造差が、対応演算子とサイズの差になって表れます。

| 観点            | jsonb\_ops（既定） | jsonb\_path\_ops |
| ------------- | -------------- | ---------------- |
| 索引化の対象        | キーと値の両方        | 値のみ              |
| キー存在の3演算子     | 対応する           | 対応しない            |
| 包含とjsonpath一致 | 対応する           | 対応する             |
| 索引サイズ         | 大きい            | 同じデータでかなり小さい     |
| 弱点            | 頻出キーで特異性が落ちる   | 値なし構造を拾えない       |

選び方は明快です。キーの有無を条件にする問い合わせが要らないなら`jsonb_path_ops`を選ぶ。索引が小さく特異性も高いため、頻出キーを含む条件では既定より速く返ります。ただし固有の穴があり、値を1つも含まない空のオブジェクト構造には索引項目が作られず、その形を探す問い合わせは全索引走査に落ちる。空の設定オブジェクトを持つ設計が混ざるなら既定側を選びます。索引の内部構造は[PostgreSQLにおけるGINインデックスの仕組みと内部構造](https://www.issoh.co.jp/tech/details/8889/)で扱いました。

### 範囲検索と並べ替えを式インデックスで拾うときの対象キーの選び方

GIN索引が拾えない条件は3つ。値の大小比較、範囲指定、並べ替えです。GINは「その値を含む行はどれか」に答える構造で、順序の情報を持ちません。金額が一定以上の行を絞る、日時で並べるといった問い合わせは索引なしの走査になります。

この穴は式インデックスで埋めます。矢印2本で取り出した式に必要な型へのキャストを挟み、btree索引を張る形です。対象キーは2条件を両方満たすものだけに絞る。全行のうち十分な割合に存在すること、値の種類がある程度ばらけていることです。ステータスのように3種類しか値を取らないキーへ張っても効きません。判断の考え方は[カーディナリティの高低の見分け方とインデックス設計](https://www.issoh.co.jp/tech/details/14785/)と同じで、JSON列だから別の基準があるわけではありません。

### btree索引の行サイズ上限とjsonb列全体に索引を張る場面の判断

`jsonb`はbtreeとhashの索引も張れます。ただし公式ドキュメントが示す用途は「JSON文書全体の等価性を確認することに意味がある場合」に限られる。btreeでの並び順はオブジェクトから順に型ごとに定義されていますが、業務上の並び順として意味を持つ場面は少ない。

技術的な上限もあります。btree索引の1行あたりのサイズは、8キロバイトのページのおよそ3分の1にあたる2,704バイトまで。大きめのJSON文書を列全体で索引化しようとすると、この上限に当たって失敗します。重複を排除したいだけなら、ハッシュ値を生成列で持って一意制約を張るほうが実務的です。投入側は[ON CONFLICTの競合ターゲット指定とMERGE文の使い分け](https://www.issoh.co.jp/tech/details/16978/)にまとめました。

## 正規化列とJSONB列の設計境界とJSONBを採用しない3つの条件

ここからは判断です。JSONBの採否は好みの問題として扱われがちですが、運用に入ってから戻せなくなる種類の決定なので条件で切ります。

### 検索・集計・制約の対象になる項目を正規化列へ出す判断の分かれ目

境界は1本で引けます。その項目が、絞り込み・並べ替え・集計・他テーブルとの突合・一意制約や外部キーのいずれかに関わるなら列へ出す。関わらないなら、JSONBに残してよい。

受注データで「取引先ごとに必要な付帯項目が違う」という要件は、付帯項目をJSONBへ寄せる典型です。一方、その中の「納品予定日」を月次集計する要件が加わったなら、納品予定日は列へ出す対象になる。項目単位で判断するのが要点で、テーブル単位に「この表はJSON方式」と決めると破綻します。JSON列の設計は[データベース正規化の第1〜第3正規形と非正規化の判断](https://www.issoh.co.jp/tech/details/16281/)の外側ではなく、非正規化をどこまで許すかという同じ問いの延長です。

採用しない条件は3つです。構造が案件開始時点で確定していて今後も変わらないとき。その項目に一意制約や外部キーを付ける必要があるとき。その列を軸に定期的な集計バッチが走るとき。いずれかならJSONBは過剰で、素直に列を切ります。

### 行全体が書き換わるMVCCとTOASTから見積もる更新コストの上振れ

PostgreSQLの更新は行の新しいバージョンを書き出す方式で、JSONの1キーだけを`jsonb_set`で書き換えても、その行のJSONB列は全体が書き直されます。

ここにTOASTが重なる。1行がおよそ2キロバイトを超えると大きな列は圧縮され、収まらなければ別の領域へ退避します。数十キロバイトのJSON文書を頻繁に更新する構成では、更新のたびに圧縮と退避の書き込みが発生し、バキュームの負荷も上がる。更新頻度が高いなら、書き換わる部分だけを別テーブルか別列へ切り出します。「JSONにまとめたほうが書き込みが減る」という直感は、ここでは逆に働きます。

### キー単位の統計が無いために起きる行数見積りのずれと実行計画の崩れ

3つ目の落とし穴は実行計画です。プランナはJSONB列を1つの値として統計を取ります。キーごとに値の分布を持つわけではないため、包含条件で何行返るかの見積りは実データの偏りを反映しません。

結果として、数行しか該当しない条件なのに大量の行を見込んで結合方式を選ぶ、あるいはその逆が起きる。`EXPLAIN ANALYZE`で見積り行数と実行行数の桁が合っているかを見るのが最初の一手で、桁がずれているなら索引の有無以前に見積りの問題です。対処は、絞り込みの主軸になるキーを式インデックスか生成列で外へ出し、統計を取れる形にすること。JSONBの条件を後段へ回す手より安定します。

### 生成列で切り出してJSONB列と正規化列を併存させる折衷案の条件

二択ではありません。生成列を使えば、JSONB列を正としたまま特定のキーだけを通常の列として見せられます。定義はJSONBからの取り出し式で書き、そこにbtree索引や一意制約、NOT NULL制約を付けられる。

向くのは、項目の追加が頻繁に起きる一方、そのうち数個だけが検索や制約の対象になるケースです。逆に生成列が10個を超えたあたりからは、正規化した表に分けたほうが読みやすい。JSONB列を正とする意味が薄れるためです。

切り出すキーが確定しないまま設計に入ると、後から列を足す作業が本番のロック時間として跳ね返る。データ構造と検索要件を先に洗い出す工程は、分析用のデータ基盤を並行して立てる案件ほど重くなるため、当社では[データ分析基盤の構築とMLOpsの支援](https://www.issoh.co.jp/service/ai/data-platform/)の中で、投入元のスキーマ整理と保持形式の決定をまとめて引き受けています。片方だけ決めても後戻りが出るためです。

## よくある質問

JSONBの採用と索引設計でよく出る5つの質問に答えます。

### jsonとjsonbはどちらを選べばよいですか？

特殊な理由がなければ`jsonb`です。索引を張れるのは`jsonb`だけで、再解析が起きない分だけ読み出しも速くなります。`json`を選ぶのは、空白やキーの順序をそのまま保全する監査目的の保管か、検索に使わない生ログの置き場だけ。迷ったら、その列にWHERE句を書く予定があるかで決めてください。

### jsonb\_opsとjsonb\_path\_opsはどちらで作成すべきですか？

キーの有無そのものを条件にする問い合わせが無いなら`jsonb_path_ops`です。索引が小さくなり、頻出キーを含む条件での特異性も高い。ただし値を含まない空のオブジェクトを検索対象にする場合は全索引走査になるため、その構造が混ざるなら既定の`jsonb_ops`を選びます。両方を張ることもできますが、更新コストは二重です。

### JSONBのキーで並べ替えたいときはどうしますか？

GIN索引では並べ替えを拾えません。矢印2本でテキストとして取り出し、必要な型へキャストした式にbtreeの式インデックスを張ります。並べ替えが恒常的なら、そのキーを生成列として外へ出し、通常の列と同じに索引を張るほうが安定する。式インデックスは問い合わせ側の式が定義と完全一致したときにしか使われません。

### JSONB列に一意制約や外部キーを付けられますか？

列そのものには実質的に付けられません。JSON文書全体の等価性でよければbtree索引を張れますが、1行あたり2,704バイトの上限に当たります。実務的な解は生成列で、JSONBから取り出した値を定義し、そこに一意制約やNOT NULL制約、外部キーを付ける。制約が必要と分かっている項目は、はじめから正規化した列として設計するほうが手戻りが少なくて済みます。

### MySQLのJSON型やMongoDBと比べてどう違いますか？

MySQLのJSON型もバイナリで格納しますが索引の張り方が異なり、文書全体を対象とするGINのような索引ではなく生成列経由が基本です。詳細は[PostgreSQLとMySQLのデータ型と全文検索の比較](https://www.issoh.co.jp/tech/details/3545/)にまとめました。[MongoDBのドキュメント指向データベースとしての構造](https://www.issoh.co.jp/column/details/3006/)との違いは、スキーマを持つ表とJSON列を同じトランザクションで扱えるかどうか。付帯情報を足すならJSONB、文書そのものが主データならドキュメント指向です。

## 関連記事

- [PostgreSQLにおけるGINインデックスとは何か？その特徴と基本概要および役割について徹底解説](https://www.issoh.co.jp/tech/details/8889/)：選んだオペレータクラスが索引の内部構造としてどう動くか
- [データベース正規化とは？第1〜第3正規形の手順と非正規化の判断を解説](https://www.issoh.co.jp/tech/details/16281/)：列へ出すかを判断する前提の正規形
- [JSON Schemaとは？JSONの構造を検証する書き方とDraft 2020-12の実装](https://www.issoh.co.jp/tech/details/16289/)：型では保証できない構造の検証方法
- [PostgreSQLのUPSERT実装｜ON CONFLICTの競合ターゲット指定とMERGE文の使い分け](https://www.issoh.co.jp/tech/details/16978/)：JSONB列を含む行の投入と重複制御
- [PostgreSQLとMySQLの違いを徹底比較｜性能・データ型・全文検索・移行と使い分け](https://www.issoh.co.jp/tech/details/3545/)：JSON型を含む2つのRDBMSの設計差

---

出典: [PostgreSQL JSONBの使い方｜json型との格納差・GIN索引と正規化列の設計境界](<https://www.issoh.co.jp/tech/details/16990/>)（株式会社一創）
