[アップデート] Amazon Aurora DSQLがjsonbデータ型をサポートしました
こんにちは。サービス開発室の武田です。
前回「Amazon Aurora DSQL の json 型対応と TEXT 移行」で、Aurora DSQLのjson型対応と、その時点で残っていた制約を確認しました。jsonb型はカラム定義に使えず(datatype jsonb not supported)、JSON値にもインデックスを張れない状態でした。TEXTに退避していたカラムをjsonには戻せるようになったものの、PostgreSQLで素直に使いたいjsonbはまだ保存できない段階です。
そのjsonbデータ型(と圧縮)への対応が発表されました。
前記事で残課題として挙げた点が、今回のアップデートでどこまで解消されるのか。ap-northeast-1に新規クラスターを作って確認してみました。本記事の数値・エラー文言・DPU見積もりは、すべてこの検証で得た値です。
先に結論
jsonb列を定義できるようになり、もっとも困っていた点が解消されました。正規化・主要演算子・戻り型・jsonpath・CHECK / DEFAULT / NOT NULL・圧縮も、確認した範囲で利用できています。
一方で次の制約は残ります。
jsonbを検索キーにできない(GIN・式インデックス・jsonb列キーのいずれも不可)- 生成列の後付けが不可
JSON内部値の検索は 生成列 + 通常インデックス で回避できます。EXPLAIN ANALYZE VERBOSEのDPU見積もりでは、等値検索でおよそ10倍の差が出ました。ただし、この回避策には前提が付きます。
- 新規テーブル設計時に抽出キーを決めておく
- 数値は数値型の生成列にする
- 索引キーは1KiB以内
AWSの発表はjsonbの保存と圧縮対応で、インデックスでの解決には触れていません。
実務目線では「保存と表現の課題は解消、検索インデックスと移行系の制約はまだ残る」という整理になります。
検証環境
- クラスター: ap-northeast-1に新規作成
- エンジン: PostgreSQL 16互換(
SELECT version()でPostgreSQL 16) - 接続: psql(libpq 18.3)+ IAM認証トークン、ユーザー admin、DB postgres、
sslmode=require
後述するDPUの値はEXPLAIN ANALYZE VERBOSEが出力する見積もりで、CloudWatchや請求の値とは一致しません。ステートメント単位の方向性比較として扱ってください。
前記事のおさらい:TEXT退避と、json止まりだった課題
前記事の時点で、DSQLでJSONを扱おうとすると次の制約にあたっていました。
jsonbでカラム定義できない(datatype jsonb not supported)- JSON値にインデックスが張れない(GIN不可・式 / 関数インデックス不可)。結果として内部条件での検索が全件走査になりDPUが増える
ALTER COLUMN ... TYPEが不可で、TEXT→ JSONの移行が難しいDROP COLUMNが不可で、移行後にデッドカラムが残る- 1トランザクション1 DDL、DDL / DMLの混在も不可
- 圧縮はデフォルトで有効
json型対応で1の一部(jsonとしての保存)は解けたものの、jsonbはまだ保存できず、2以降はそのまま残っていました。今回のアップデートで、ここがどう変わったかを順に見ていきます。
今回のアップデートで解消されたこと
jsonb列を定義できる
今回の中心となる変更です。これまで弾かれていたjsonb列のCREATE TABLEが通るようになりました。
CREATE TABLE t (id int PRIMARY KEY, data jsonb);
-- 成功(旧: datatype jsonb not supported は解消)
jsonb正規化:重複キー除去とキー順の非保持
jsonbらしい正規化も効いています。同じJSONをjsonb列とjson列に入れて比較しました。
-- INSERTする値
{"b":2,"a":1,"a":3,"z":9}
jsonb列:{"a": 3, "b": 2, "z": 9}(重複キーは最後の値が残る。キー順は保持されず、今回の出力ではソートされて見える)json列: 入力どおりの原文を保持
PostgreSQLのjson / jsonbの挙動どおりです。
主要演算子・戻り型・jsonpath
演算子・戻り型・jsonpathも、確認した範囲でPostgreSQL標準どおりに動きました。
SELECT data->>'a', data @> '{"a":3}', data ? 'a' FROM t;
-- a=3, contains=t, haskey=t
SELECT pg_typeof(data->'a'), pg_typeof(data->>'a') FROM t;
-- -> は jsonb、->> は text
SELECT data @? '$.a', data @@ '$.a == 3', jsonb_path_exists(data, '$.a') FROM t;
-- t, t, t
->がjsonb、->>がtextを返す点も確認できました。なおjsonpathは@?・@@・jsonb_path_existsの3つを確認したもので、全jsonpathを網羅したわけではありません。
CHECK / DEFAULT / NOT NULLでスキーマ制約をかけられる
実務で効いてくるのが、jsonbに対する制約です。「オブジェクトであること」をDB側で担保できます。
CREATE TABLE t (
id int PRIMARY KEY,
data jsonb NOT NULL DEFAULT '{}'::jsonb
CHECK (jsonb_typeof(data) = 'object')
);
DEFAULT '{}'は適用され、配列を入れようとするとCHECK違反(SQLSTATE 23514)で拒否されました。TEXT退避時代はDBとしてJSONであることを保証できませんでしたが、INSERT時点で型と構造を弾けます。
既存テーブルへのjsonb列追加はできる
列の「追加」は可能です。
ALTER TABLE t ADD COLUMN extra jsonb; -- 成功
後述のALTER COLUMN ... TYPEや生成列の後付けはできませんが、jsonb列を新たに足すだけなら実行できます。移行ストーリーに関わるため、のちほどあらためて触れます。
圧縮:デフォルトで効き、STORAGEで無効化できる
大きな値はデフォルトで圧縮されます。約27KBの高圧縮データをjson / jsonbの両方に入れ、pg_column_sizeで内部サイズを確認しました。
| 列 | pg_column_size |
|---|---|
| json | 139B |
| jsonb | 146B |
約27KBが100バイト台まで縮んでいます(高圧縮データのケース)。pg_column_sizeは内部サイズであり、請求対象のストレージそのものではない点に注意してください。
圧縮の有無はSTORAGEで制御できます。約9KBの値を、STORAGEを変えたテーブルにそれぞれINSERTして比較しました。
| STORAGE | pg_column_size |
|---|---|
| MAIN(圧縮あり) | 76B |
| EXTERNAL | 9017B |
| PLAIN | 9017B |
PLAIN / EXTERNALでは圧縮が効かず、元サイズのまま格納されました。圧縮しない格納を明示したい場合の制御点として使えます。なおALTER COLUMN SET STORAGEは既存行を即座に再圧縮するとは限らないため、挙動を比べるならSTORAGE指定済みのテーブルにINSERTするのが確実です。
それでも残る制約
ここからが続報の本題です。保存と表現の課題は解けましたが、検索インデックスと移行系の制約は残っています。
jsonbを検索キーにできない(GIN / 式 / jsonb列キー)
JSON内部値を直接インデックス検索する手段は、今回も確認できませんでした。3方向すべてで弾かれます。
-- GIN(PostgreSQL の jsonb 定番インデックス)
CREATE INDEX ASYNC idx ON t USING gin (data);
-- USING not supported for CREATE INDEX
CREATE INDEX ASYNC idx ON t USING gin (data jsonb_path_ops);
-- USING not supported for CREATE INDEX
-- 式インデックス
CREATE INDEX ASYNC idx ON t ((data->>'a'));
-- expressions as index keys not supported
-- jsonb 列そのものをキーに
CREATE INDEX ASYNC idx ON t (data);
-- datatype jsonb is not supported in a key
USING ...構文自体がDSQLのCREATE INDEXに存在しないため、GINは「PostgreSQLの移行DDLがそのままは通らない例」ととらえるのが正確です。また式インデックスはJSON固有の失敗ではなく、JSONと無関係なlower(name)でも同じくexpressions as index keys not supportedになりました。式インデックス一般がサポートされていません。
CREATE INDEX ASYNC idx ON te (lower(name));
-- expressions as index keys not supported
整理すると、jsonbを検索キーにできない(GIN・式インデックス・jsonb列キーのいずれも不可)ため、JSON内部値を直接インデックス検索できません。なお今回の検証では、少なくともPKでjsonb列がINCLUDEされる形で保持されることを確認しています(検索キーではありません)。「インデックスに一切載せられない」のではなく「検索キーにできない」が正確な言い方です。
ALTER COLUMN TYPE / DROP COLUMN は引き続き不可
TEXT→ jsonbへの型変換も、列の削除もできません。
ALTER TABLE t ALTER COLUMN data TYPE jsonb USING data::jsonb;
-- unsupported ALTER TABLE ALTER COLUMN ... SET DATA TYPE statement
ALTER TABLE t DROP COLUMN data;
-- unsupported ALTER TABLE DROP COLUMN statement
前記事で挙げた「TEXT退避カラムをその場でjsonbに変える」「移行後のデッドカラムを消す」は、今回も成立しません。
生成列の後付けも不可
後述の回避策で使う生成列は、既存テーブルへの後付けができません。
ALTER TABLE tg ADD COLUMN a_gen text GENERATED ALWAYS AS (data->>'a') STORED;
-- ALTER TABLE ADD COLUMN with constraint not supported
生成列を使えるのは新規テーブル作成時だけ、という点が移行設計に効いてきます。
回避策: 生成列 + 通常インデックス
式インデックスは張れませんが、GENERATED ... STOREDで抽出値を別の列に保存すれば、通常のスカラ列としてインデックスを張れます。位置付けは「GIN / 式インデックスの代替」ではなく、事前に決めたキーをリレーショナル列へ昇格する設計パターン です。
CREATE TABLE tgen (
id int PRIMARY KEY,
data jsonb,
a_extracted text GENERATED ALWAYS AS (data->>'a') STORED -- 新規テーブル作成時のみ可
);
CREATE INDEX ASYNC idx_gen_a ON tgen (a_extracted); -- 成功(job_id 返却、indisvalid=t)
EXPLAIN ANALYZE VERBOSEでの比較(等値検索)
1000行を投入したtgenで、生成列インデックスを使う場合とjsonbを直接条件にする場合を比べました。Execution Timeは実測、DPUはEXPLAIN ANALYZE VERBOSEが返すStatement DPU Estimateの値です(請求値とは一致しません)。
生成列の条件 WHERE a_extracted = '500':
Index Scan using idx_gen_a (Index Cond: a_extracted = '500')
actual rows=1
Execution Time: 1.701 ms
Statement DPU Estimate: Total 0.01088 (Read 0.00290)
jsonb 直接の条件 WHERE data->>'a' = '500':
Index Only Scan using tgen_pkey
Filter: ((data->>'a') = '500')
Rows Removed by Filter: 999
actual rows=1
Execution Time: 6.704 ms
Statement DPU Estimate: Total 0.10681 (Read 0.09685)
| 条件 | プラン | Filter 除外行 | Execution Time | DPU 見積もり Total | Read |
|---|---|---|---|---|---|
生成列 a_extracted='500' |
Index Scan(Index Cond) | - | 1.701 ms | 0.01088 | 0.00290 |
jsonb 直接 data->>'a'='500' |
Index Only Scan(pkey)+ Filter | 999 | 6.704 ms | 0.10681 | 0.09685 |
jsonb直接の条件はIndex Condにならず、PKカバリングインデックス(tgen_pkey)を全件走査してFilterで除外しています。1行を取り出すのに999行を捨てている形です。結果として、等値(高選択性)ではDPU見積もりで約9.8倍、Readで約33倍、実行時間で約3.9倍の差が出ました。なお本検証は1000行での比較です。実運用での評価は、実データ量・選択性・繰り返し実行を踏まえて確認してください。
DSQLでは主キーが全列INCLUDEのカバリングインデックスとして働きます。そのためPostgreSQLならSeq Scanになる場面でもIndex Only Scan using *_pkeyに見えます。プランの文字列ではなく、JSON条件がIndex Condに乗っているか、Rows Removed by Filterの大きさ、DPU見積もりの差を見るのが妥当です。
数値キーは数値型の生成列にする
注意点として、->>はtextを返します。textのまま範囲比較すると文字列順比較になり、意図とずれます。
-- text のまま範囲比較(文字列順)
WHERE a_txt > '900' -- 108 件
-- 数値型の生成列で範囲比較
WHERE a_num > 900 -- 100 件
数値範囲を扱うなら、intなどの数値型で生成列を作ります(作成自体は可能です)。
a_num int GENERATED ALWAYS AS ((data->>'a')::int) STORED
100行ヒットする数値範囲で比較しました。
| 条件 | プラン | Filter除外行 | Execution Time | DPU見積もり Total | Read |
|---|---|---|---|---|---|
数値生成列 a_num>900 |
Index Scan(idx_a_num) | - | 2.200 ms | 0.03741 | 0.03185 |
jsonb直接 (data->>'a')::int>900 |
Index Only Scan(pkey)+ Filter | 900 | 6.464 ms | 0.10204 | 0.08753 |
範囲(100 / 1000行)ではDPU見積もりで約2.7倍の差でした。差の大きさは選択性で変わります。等値(1行)では約10倍、範囲(100行)では約2.7倍と、ヒット行が少ない(選択性が高い)ほど生成列インデックスの優位が大きくなる傾向です。なお(data->>'a')::intはキャストで落ちる値が混ざるとINSERT / UPDATEが失敗します。不正値を許さないならJSON側にCHECKを置く、許すなら生成式をCASEでNULL化するなど、生成式が失敗しない設計にする必要があります。
回避策の制約と前提
便利な一方で、適用には前提が付きます。
- 生成列は 新規テーブル作成時のみ 定義可能(既存テーブルへの後付けは不可)
- secondary index keyは合計1KiB制限。2000バイトの抽出値をキーにするとINDEX_BUILDが
failed: key size too largeとなり、indisvalid=fのまま有効化されない。長いJSON文字列はそのまま索引キーにできない - 抽出キーが事前に分かっている必要がある。動的・任意キーのアドホック検索には向かない
data->>'key'は欠損キーやJSON nullでSQL NULLになる。UNIQUE索引ではNULLS [NOT] DISTINCTの扱いに注意
既存テーブル移行の現実解
移行で問題になるのは、生成列が新規テーブル作成時にしか定義できない点です。ALTER COLUMN ... TYPEとDROP COLUMNも不可のままです。一方で、制約なしのjsonb列をADD COLUMNで足すこと自体はできます。これらを踏まえると、既存テーブルの移行は次の選択肢になります。
- テーブル再作成: 生成列付きで新テーブルを作り、データを入れ替える。生成列 + インデックスを最初から使える。後述の1トランザクション行数上限があるため、データ移行はバッチ分割が必要
- 別列運用: 既存テーブルに
jsonb列をADD COLUMNし、検索キーは別の通常列としてアプリケーション側で書き込む。生成列の自動同期は使えないため、アプリケーション側で値の整合を担保する - アプリケーション側の二重書き込み: JSON本体と抽出キーを、アプリケーションのロジックで同時に書き込む
いずれも、検索キーをどうもつかをアプリケーション設計に織り込む必要があります。ALTER COLUMN ... TYPEが使えない以上、TEXT列を後からjsonbに変える方法は今回も見つかりませんでした。
DSQL特有の注意点
検証中にあたった、JSONに限らないDSQLの挙動も挙げておきます。
CREATE INDEXは非同期。CREATE INDEX ASYNCを使う(同期だとunsupported mode. please use CREATE INDEX ASYNC)- 非同期ジョブは
sys.jobs(job_id、status、job_type=INDEX_BUILD / ANALYZE / DROP)で管理。完了・失敗もここで確認する。CREATE INDEX ASYNCは返っても成功とは限らず、pg_index.indisvalidで確認する必要がある - 完了待ちは
sys.wait_for_job(job_id)。これはprocedureなのでCALL sys.wait_for_job(...)で呼ぶ(SELECTでは呼べない) - PKは全列INCLUDEのカバリングインデックスとして働く。PostgreSQLならSeq Scanになる場面でも、DSQLでは
Index Only Scan using *_pkey+ Filterに見えることがある - 1トランザクションの行数上限がある。
INSERT ... generate_series(1,5000)はtransaction row limit exceeded(SQLSTATE54000)で失敗し、1000行なら通った。大量投入はバッチ分割が必要 - DPUは
EXPLAIN ANALYZE VERBOSEでのみ見積もりが出る。NOTICEのとおりCloudWatchや請求とは一致せず、ステートメント単位の方向性比較用。本記事の倍率もこの見積もり上の比較になる
まとめ
2026-06-08のjsonb対応で、前記事で挙げた課題の多くが解消しました。中心はjsonb列を定義できるようになった点です。合わせて正規化・主要演算子・戻り型・jsonpath・CHECK / DEFAULT / NOT NULL・圧縮も、確認した範囲で利用できました。TEXTに押し込む必要はなくなり、サーバーサイドでjsonbを素直に扱えます。
一方で、次の制約は残ります。
jsonbを検索キーにできない(GIN・式インデックス・jsonb列キーのいずれも不可)。JSON内部値の直接インデックス検索はできない- 生成列の後付けが不可
JSON内部値の検索は 生成列 + 通常インデックス で回避できます。EXPLAIN ANALYZE VERBOSEのDPU見積もりでは、等値で約10倍、範囲で約2.7倍の差が出ました。ただし、この回避策には「新規テーブル設計時に抽出キーを決める」「数値は数値型の生成列にする」「索引キーは1KiB以内」という前提が付きます。任意キーをあとから自由に検索できるわけではなく、アクセスパターンを設計時点で決めておく必要があります。
AWSの発表はjsonbの保存と圧縮対応で、インデックスでの解決には触れていません。前記事から一歩進んで、列定義の課題は解消されました。JSON内部検索とスキーマ移行は、引き続きリレーショナル列への昇格・再作成戦略・アプリケーション側の設計で引き受ける前提に立つのがよさそうです。





