PostgreSQLでJSONを列に入れるとき、型の選択肢はjsonとjsonbの2つ。名前は一文字違いですが、格納形式が別物で、索引を張れるかどうかも分かれます。この記事では格納差から入り、取り出しと判定の演算子、GIN索引のオペレータクラスの選び方、項目を正規化列へ出すかJSONB列に残すかの境界までを、18系(2026年8月時点の最新マイナーは18.6)の仕様に沿って整理しました。全般の位置づけはデータベースとは?種類・DBMS・RDBとNoSQLの選び方に譲ります。
まとめ: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でデータ型と全文検索がどう違うかに整理しました。なお、どちらの型もJSON文書としての妥当性しか検査しません。「この列にはuser_idが必ず入る」といった構造の保証がほしいなら、JSON Schemaで構造を検証する書き方をアプリケーション側に置くか、後述の生成列と制約で受けます。
戻り値の型と判定範囲で使い分ける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の対応バージョンと拡張機能の統制にまとめました。
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インデックスの仕組みと内部構造で扱いました。
範囲検索と並べ替えを式インデックスで拾うときの対象キーの選び方
GIN索引が拾えない条件は3つ。値の大小比較、範囲指定、並べ替えです。GINは「その値を含む行はどれか」に答える構造で、順序の情報を持ちません。金額が一定以上の行を絞る、日時で並べるといった問い合わせは索引なしの走査になります。
この穴は式インデックスで埋めます。矢印2本で取り出した式に必要な型へのキャストを挟み、btree索引を張る形です。対象キーは2条件を両方満たすものだけに絞る。全行のうち十分な割合に存在すること、値の種類がある程度ばらけていることです。ステータスのように3種類しか値を取らないキーへ張っても効きません。判断の考え方はカーディナリティの高低の見分け方とインデックス設計と同じで、JSON列だから別の基準があるわけではありません。
btree索引の行サイズ上限とjsonb列全体に索引を張る場面の判断
jsonbはbtreeとhashの索引も張れます。ただし公式ドキュメントが示す用途は「JSON文書全体の等価性を確認することに意味がある場合」に限られる。btreeでの並び順はオブジェクトから順に型ごとに定義されていますが、業務上の並び順として意味を持つ場面は少ない。
技術的な上限もあります。btree索引の1行あたりのサイズは、8キロバイトのページのおよそ3分の1にあたる2,704バイトまで。大きめのJSON文書を列全体で索引化しようとすると、この上限に当たって失敗します。重複を排除したいだけなら、ハッシュ値を生成列で持って一意制約を張るほうが実務的です。投入側はON CONFLICTの競合ターゲット指定とMERGE文の使い分けにまとめました。
正規化列とJSONB列の設計境界とJSONBを採用しない3つの条件
ここからは判断です。JSONBの採否は好みの問題として扱われがちですが、運用に入ってから戻せなくなる種類の決定なので条件で切ります。
検索・集計・制約の対象になる項目を正規化列へ出す判断の分かれ目
境界は1本で引けます。その項目が、絞り込み・並べ替え・集計・他テーブルとの突合・一意制約や外部キーのいずれかに関わるなら列へ出す。関わらないなら、JSONBに残してよい。
受注データで「取引先ごとに必要な付帯項目が違う」という要件は、付帯項目をJSONBへ寄せる典型です。一方、その中の「納品予定日」を月次集計する要件が加わったなら、納品予定日は列へ出す対象になる。項目単位で判断するのが要点で、テーブル単位に「この表はJSON方式」と決めると破綻します。JSON列の設計はデータベース正規化の第1〜第3正規形と非正規化の判断の外側ではなく、非正規化をどこまで許すかという同じ問いの延長です。
採用しない条件は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の支援の中で、投入元のスキーマ整理と保持形式の決定をまとめて引き受けています。片方だけ決めても後戻りが出るためです。
よくある質問
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のデータ型と全文検索の比較にまとめました。MongoDBのドキュメント指向データベースとしての構造との違いは、スキーマを持つ表とJSON列を同じトランザクションで扱えるかどうか。付帯情報を足すならJSONB、文書そのものが主データならドキュメント指向です。
関連記事
- PostgreSQLにおけるGINインデックスとは何か?その特徴と基本概要および役割について徹底解説:選んだオペレータクラスが索引の内部構造としてどう動くか
- データベース正規化とは?第1〜第3正規形の手順と非正規化の判断を解説:列へ出すかを判断する前提の正規形
- JSON Schemaとは?JSONの構造を検証する書き方とDraft 2020-12の実装:型では保証できない構造の検証方法
- PostgreSQLのUPSERT実装|ON CONFLICTの競合ターゲット指定とMERGE文の使い分け:JSONB列を含む行の投入と重複制御
- PostgreSQLとMySQLの違いを徹底比較|性能・データ型・全文検索・移行と使い分け:JSON型を含む2つのRDBMSの設計差