[アップデート] Amazon Aurora DSQLがjsonbデータ型をサポートしました

[アップデート] Amazon Aurora DSQLがjsonbデータ型をサポートしました

2026.06.09

こんにちは。サービス開発室の武田です。

前回「Amazon Aurora DSQL の json 型対応と TEXT 移行」で、Aurora DSQLのjson型対応と、その時点で残っていた制約を確認しました。jsonb型はカラム定義に使えず(datatype jsonb not supported)、JSON値にもインデックスを張れない状態でした。TEXTに退避していたカラムをjsonには戻せるようになったものの、PostgreSQLで素直に使いたいjsonbはまだ保存できない段階です。

そのjsonbデータ型(と圧縮)への対応が発表されました。

https://aws.amazon.com/about-aws/whats-new/2026/06/amazon-aurora-dsql-supports-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を扱おうとすると次の制約にあたっていました。

  1. jsonbでカラム定義できない(datatype jsonb not supported
  2. JSON値にインデックスが張れない(GIN不可・式 / 関数インデックス不可)。結果として内部条件での検索が全件走査になりDPUが増える
  3. ALTER COLUMN ... TYPEが不可で、TEXT→ JSONの移行が難しい
  4. DROP COLUMNが不可で、移行後にデッドカラムが残る
  5. 1トランザクション1 DDL、DDL / DMLの混在も不可
  6. 圧縮はデフォルトで有効

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 は引き続き不可

TEXTjsonbへの型変換も、列の削除もできません。

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 ... TYPEDROP 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.jobsjob_idstatusjob_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(SQLSTATE 54000)で失敗し、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内部検索とスキーマ移行は、引き続きリレーショナル列への昇格・再作成戦略・アプリケーション側の設計で引き受ける前提に立つのがよさそうです。

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事