JSON_TABLE は、Oracle DatabaseのJSONデータをリレーショナルな行・列として展開するSQL/JSON関数です。PL/SQLの取り込みバッチと組み合わせると、APIレスポンスやJSON連携ファイルをステージングし、検証し、正規化テーブルへ MERGE する処理をSQL中心で組み立てられます。
この記事では、JSON機能全体の説明ではなく、PL/SQLでJSONを取り込む実務レシピに絞って解説します。JSON関数全体の整理は Oracle JSON完全ガイド、APIからJSONを取得する処理は UTL_HTTP完全ガイド もあわせて確認してください。
JSON_TABLEでJSONを行・列へ展開する基本NESTED PATHで配列を明細行へ変換する方法ON ERROR/ON EMPTYの使い分け- ステージング表、正規化表、
MERGEによる取り込み DBMS_ERRLOG、再実行、重複排除、性能設計- PL/SQL例外処理とログ設計
JSON_TABLEの基本
JSON_TABLE は FROM 句で使い、JSON文書に対して行パスと列定義を指定します。列ごとにJSONパス、SQLデータ型、欠損時や変換エラー時の動作を指定できるため、JSONをSQLで扱う入口として非常に便利です。
JSON_TABLE(payload, '$' ...) の '$' が文書全体を表します。COLUMNS でJSONキーをSQL列へ投影します。NESTED PATH で配列要素を複数行へ展開します。NULL ON ERROR ですが、重要列は明示します。ステージング表を用意する
実務では、JSONをいきなり本テーブルへ入れるより、まず受信JSONをそのままステージング表へ保存する設計が安定します。取り込み単位、受信日時、処理状態、エラーメッセージを持たせると、再実行や障害調査が楽になります。
CREATE TABLE order_json_stage (
stage_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
source_name VARCHAR2(100) NOT NULL,
payload CLOB CONSTRAINT order_payload_is_json CHECK (payload IS JSON),
status VARCHAR2(20) DEFAULT 'READY' NOT NULL,
error_message VARCHAR2(4000),
created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
processed_at TIMESTAMP
);
INSERT INTO order_json_stage(source_name, payload)
VALUES('api/orders', '{
"orderId": "A1001",
"customer": {"id": 101, "name": "Sato"},
"orderDate": "2026-05-27T10:15:00",
"items": [
{"sku":"P-001","qty":2,"price":1200.5},
{"sku":"P-002","qty":1,"price":980}
],
"note": null
}');
COMMIT;
JSONをCLOBで持つ場合、サイズや変換処理にも注意します。LOB処理の基本は DBMS_LOB完全ガイド が参考になります。
単層キーを列に展開する
まずは注文ヘッダーのような単層データを列へ展開します。数値や日時に変換する列は、異常値が入ったときにどう扱うかを明示します。
SELECT s.stage_id,
j.order_id,
j.customer_id,
j.customer_name,
j.order_ts
FROM order_json_stage s
CROSS APPLY JSON_TABLE(
s.payload,
'$'
COLUMNS (
order_id VARCHAR2(30) PATH '$.orderId' ERROR ON EMPTY,
customer_id NUMBER PATH '$.customer.id' ERROR ON ERROR,
customer_name VARCHAR2(100) PATH '$.customer.name' NULL ON EMPTY,
order_ts TIMESTAMP PATH '$.orderDate' NULL ON ERROR
)
) j
WHERE s.status = 'READY';
必須キーは ERROR ON EMPTY、任意項目は NULL ON EMPTY、異常値を落としたい列は NULL ON ERROR など、列の意味に合わせて使い分けます。
NESTED PATHで配列を明細行にする
明細配列は NESTED PATH で展開します。親の注文IDと子の明細項目を同じ JSON_TABLE の中で定義すると、親子の関係が読みやすくなります。
SELECT s.stage_id,
j.order_id,
j.line_no,
j.sku,
j.qty,
j.price
FROM order_json_stage s
CROSS APPLY JSON_TABLE(
s.payload,
'$'
COLUMNS (
order_id VARCHAR2(30) PATH '$.orderId' ERROR ON EMPTY,
NESTED PATH '$.items[*]'
COLUMNS (
line_no FOR ORDINALITY,
sku VARCHAR2(50) PATH '$.sku' ERROR ON EMPTY,
qty NUMBER PATH '$.qty' DEFAULT 0 ON ERROR,
price NUMBER PATH '$.price' NULL ON ERROR
)
)
) j
WHERE s.stage_id = 1;
FOR ORDINALITY を使うと、配列内の順序を行番号として取得できます。明細の並び順が業務的に意味を持つ場合は、取り込み先にも行番号を保存しておくと再現性が上がります。
取り込み先テーブルを作る
JSONをそのまま使い続けるより、検索や集計でよく使う項目は正規化テーブルへ落とす方が扱いやすいことが多いです。ここでは注文ヘッダーと明細テーブルへ分けます。
CREATE TABLE orders_h ( order_id VARCHAR2(30) PRIMARY KEY, customer_id NUMBER NOT NULL, customer_name VARCHAR2(100), order_ts TIMESTAMP, source_stage_id NUMBER, updated_at TIMESTAMP DEFAULT SYSTIMESTAMP ); CREATE TABLE orders_d ( order_id VARCHAR2(30) NOT NULL, line_no NUMBER NOT NULL, sku VARCHAR2(50) NOT NULL, qty NUMBER DEFAULT 0 NOT NULL, price NUMBER, updated_at TIMESTAMP DEFAULT SYSTIMESTAMP, CONSTRAINT orders_d_pk PRIMARY KEY (order_id, line_no), CONSTRAINT orders_d_fk FOREIGN KEY (order_id) REFERENCES orders_h(order_id) );
OracleではMERGEでUPSERTする
Oracle SQLでは、他DBのUPSERT構文をそのまま使わず MERGE に置き換えます。JSONから取り込むUPSERTは、基本的に MERGE を使います。MERGEの詳細は Oracle MERGE文完全ガイド も参考になります。
MERGE INTO orders_h d
USING (
SELECT s.stage_id,
j.order_id,
j.customer_id,
j.customer_name,
j.order_ts
FROM order_json_stage s
CROSS APPLY JSON_TABLE(
s.payload,
'$'
COLUMNS (
order_id VARCHAR2(30) PATH '$.orderId' ERROR ON EMPTY,
customer_id NUMBER PATH '$.customer.id' ERROR ON ERROR,
customer_name VARCHAR2(100) PATH '$.customer.name' NULL ON EMPTY,
order_ts TIMESTAMP PATH '$.orderDate' NULL ON ERROR
)
) j
WHERE s.status = 'READY'
) src
ON (d.order_id = src.order_id)
WHEN MATCHED THEN
UPDATE SET d.customer_id = src.customer_id,
d.customer_name = src.customer_name,
d.order_ts = src.order_ts,
d.source_stage_id = src.stage_id,
d.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN
INSERT (order_id, customer_id, customer_name, order_ts, source_stage_id)
VALUES (src.order_id, src.customer_id, src.customer_name, src.order_ts, src.stage_id);
一意制約違反が出る場合は、入力JSON内に同じキーが重複している可能性があります。ORA-00001 の切り分けやMERGEでの重複対策は ORA-00001の原因と解決方法 も関連します。
明細もMERGEで取り込む
ヘッダーを先にMERGEしたあと、明細をMERGEします。明細では order_id + line_no のようなキーを決め、再実行しても同じ結果になるようにします。
MERGE INTO orders_d d
USING (
SELECT j.order_id,
j.line_no,
j.sku,
j.qty,
j.price
FROM order_json_stage s
CROSS APPLY JSON_TABLE(
s.payload,
'$'
COLUMNS (
order_id VARCHAR2(30) PATH '$.orderId' ERROR ON EMPTY,
NESTED PATH '$.items[*]'
COLUMNS (
line_no FOR ORDINALITY,
sku VARCHAR2(50) PATH '$.sku' ERROR ON EMPTY,
qty NUMBER PATH '$.qty' DEFAULT 0 ON ERROR,
price NUMBER PATH '$.price' NULL ON ERROR
)
)
) j
WHERE s.status = 'READY'
) src
ON (d.order_id = src.order_id AND d.line_no = src.line_no)
WHEN MATCHED THEN
UPDATE SET d.sku = src.sku,
d.qty = src.qty,
d.price = src.price,
d.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN
INSERT (order_id, line_no, sku, qty, price)
VALUES (src.order_id, src.line_no, src.sku, src.qty, src.price);
PL/SQLで取り込み処理をまとめる
実務では、MERGEを1本ずつ手で実行するのではなく、PL/SQLプロシージャとしてまとめます。成功時はステージングを DONE、失敗時は ERROR にして、再実行できるようにします。
CREATE OR REPLACE PROCEDURE import_order_json(p_stage_id IN NUMBER)
AS
BEGIN
MERGE INTO orders_h d
USING (
SELECT s.stage_id,
j.order_id,
j.customer_id,
j.customer_name,
j.order_ts
FROM order_json_stage s
CROSS APPLY JSON_TABLE(
s.payload,
'$'
COLUMNS (
order_id VARCHAR2(30) PATH '$.orderId' ERROR ON EMPTY,
customer_id NUMBER PATH '$.customer.id' ERROR ON ERROR,
customer_name VARCHAR2(100) PATH '$.customer.name' NULL ON EMPTY,
order_ts TIMESTAMP PATH '$.orderDate' NULL ON ERROR
)
) j
WHERE s.stage_id = p_stage_id
) src
ON (d.order_id = src.order_id)
WHEN MATCHED THEN
UPDATE SET d.customer_id = src.customer_id,
d.customer_name = src.customer_name,
d.order_ts = src.order_ts,
d.source_stage_id = src.stage_id,
d.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN
INSERT (order_id, customer_id, customer_name, order_ts, source_stage_id)
VALUES (src.order_id, src.customer_id, src.customer_name, src.order_ts, src.stage_id);
-- 明細MERGEは別SQLとして実行する
-- merge-order-detail.sql と同じ考え方で実装
UPDATE order_json_stage
SET status = 'DONE',
processed_at = SYSTIMESTAMP,
error_message = NULL
WHERE stage_id = p_stage_id;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
UPDATE order_json_stage
SET status = 'ERROR',
processed_at = SYSTIMESTAMP,
error_message = SUBSTR(SQLERRM, 1, 4000)
WHERE stage_id = p_stage_id;
COMMIT;
RAISE;
END;
/
例外をJSON形式で返すAPI設計は 例外をJSON形式で返すREST対応API設計 と相性があります。動的SQLやバインド変数を使う場合は 動的SQLとBIND変数の最適設計 も確認してください。
ON ERRORとON EMPTYの使い分け
JSON取り込みでは、欠損値と型変換エラーを分けて考えます。欠損なら許容するのか、異常値なら落とすのか、既定値を入れるのかを列ごとに決めます。
DBMS_ERRLOGで行単位エラーを残す
すべてのエラーで処理全体を止めると、1件の不正データで大量取り込みが止まります。行単位で許容できるエラーは DBMS_ERRLOG と LOG ERRORS を検討します。詳しくは DBMS_ERRLOGの使い方 でも扱っています。
BEGIN
DBMS_ERRLOG.CREATE_ERROR_LOG(
dml_table_name => 'ORDERS_D',
err_log_table_name => 'ERR$_ORDERS_D'
);
END;
/
INSERT INTO orders_d(order_id, line_no, sku, qty, price)
SELECT j.order_id, j.line_no, j.sku, j.qty, j.price
FROM order_json_stage s
CROSS APPLY JSON_TABLE(
s.payload,
'$'
COLUMNS (
order_id VARCHAR2(30) PATH '$.orderId' ERROR ON EMPTY,
NESTED PATH '$.items[*]'
COLUMNS (
line_no FOR ORDINALITY,
sku VARCHAR2(50) PATH '$.sku' ERROR ON EMPTY,
qty NUMBER PATH '$.qty' NULL ON ERROR,
price NUMBER PATH '$.price' NULL ON ERROR
)
)
) j
WHERE s.stage_id = 1
LOG ERRORS INTO err$_orders_d ('json import') REJECT LIMIT UNLIMITED;
検索性能とインデックス
JSONを検索する頻度が高い場合は、毎回全文を展開するのではなく、検索キーを仮想列や関数インデックスにします。頻繁に集計するなら、取り込み時に正規化テーブルへ落とす方がシンプルです。
ALTER TABLE order_json_stage ADD (
order_id_v GENERATED ALWAYS AS (
JSON_VALUE(payload, '$.orderId' RETURNING VARCHAR2(30))
) VIRTUAL
);
CREATE INDEX order_json_stage_ix1
ON order_json_stage(order_id_v);
SELECT stage_id
FROM order_json_stage
WHERE order_id_v = 'A1001';
JSONを保存したまま検索するのか、正規化して検索するのかは、検索頻度、更新頻度、JSONの変化しやすさで決めます。大量の検索・集計が必要なら、JSON_TABLEは取り込み時に使い、業務検索は通常テーブルに寄せる構成が扱いやすいです。
再実行できる取り込み設計
JSON取り込みバッチは、一度失敗したら終わりではありません。再実行しても二重登録されない、途中まで成功したデータを壊さない、エラーの原因が追える、という設計が大切です。
READY、PROCESSING、DONE、ERROR を持たせます。MERGE で再実行できるようにします。本番前チェックリスト
ERROR ON EMPTY で欠損を検出する列を決めます。NESTED PATH と FOR ORDINALITY で明細順序を保存します。まとめ
JSON_TABLE を使うと、Oracle上のJSONをSQLの行・列として展開できます。PL/SQLでは、受信JSONをステージング表へ保存し、JSON_TABLE で検証・展開し、MERGE で正規化テーブルへ反映する流れが実務向きです。
重要なのは、他DB構文を混ぜずOracleの MERGE を使うこと、ON ERROR / ON EMPTY を列ごとに明示すること、再実行できるステータス管理とログを持つことです。JSON_TABLEは強力ですが、取り込みバッチではデータ品質・再実行・性能まで含めて設計しましょう。

