マルチテーブルINSERTは一度の表スキャンで複数の表へ同時に挿入できるOracle独自の拡張構文です。ETLやデータ移送、親子関係の同時作成、条件分岐での仕分けなどで威力を発揮し、PL/SQLからも通常のINSERTと同様に実行できます。ここではINSERT ALL / INSERT FIRST の基本、条件付き分岐、親子同時挿入の実例、エラーログ併用、パフォーマンスと注意点までをまとめます。
基本構文とALL/FIRSTの違い
INSERT ALL は選択された各行に対して、指定したすべての INTO 句を評価して挿入します。INSERT FIRST は最初に合致した WHEN 条件の INTO だけを実行し、それ以降の条件評価はスキップされます。どちらも「INSERT … SELECT …」の一種であり、FROM 句側のサブクエリ(またはWITH句で定義した結果)を一度読むだけで複数表に書けるため、行ループや複数回のINSERTに比べて効率的です。
-- 例:マスタとアーカイブへ同時投入(ALL)
INSERT ALL
INTO sales_main (id, amount, sale_dt) VALUES (id, amount, sale_dt)
INTO sales_archive(id, amount, sale_dt) VALUES (id, amount, sale_dt)
SELECT id, amount, sale_dt
FROM staging_sales
WHERE status = 'OK';
条件付きの仕分け:WHEN句とINSERT FIRST
条件に応じて別テーブルへ振り分ける場合はWHEN句を使います。同じ行が複数条件に合致しても全部に入れたいならINSERT ALL、最初に合致した一箇所だけに入れたいならINSERT FIRSTを選びます。評価順序は上から下へ進むため、FIRST を用いるときは条件の優先度をコード順で表現します。
-- 金額帯でテーブルを振り分け(FIRST=最初に合致した1箇所のみ)
INSERT FIRST
WHEN amount >= 100000 THEN
INTO sales_large(id, amount, sale_dt) VALUES(id, amount, sale_dt)
WHEN amount >= 10000 THEN
INTO sales_mid (id, amount, sale_dt) VALUES(id, amount, sale_dt)
ELSE
INTO sales_small(id, amount, sale_dt) VALUES(id, amount, sale_dt)
SELECT id, amount, sale_dt
FROM staging_sales
WHERE status = 'OK';
PL/SQLからの実行とトランザクション制御
PL/SQLブロック内では静的SQLとしてそのまま書けます。大量データの投入で分割コミットが必要なら、WHERE句側で範囲を区切って複数回に分けるか、コミットポリシーを外側のPL/SQLで管理します。
BEGIN
INSERT ALL
INTO target_a(col1, col2) VALUES (src.c1, src.c2)
INTO target_b(col1, col3) VALUES (src.c1, src.c3)
SELECT c1, c2, c3 FROM src_view src WHERE load_batch_id = :b1;
COMMIT; -- バッチ単位で明示コミット
END;
/
親子同時挿入:シーケンスとCURRVALの活用
親テーブルの採番IDを使って子テーブルへも同時に書きたい場合、同一行の評価中にNEXTVALとCURRVALを組み合わせると一往復で済みます。親のNEXTVALで新IDを生成し、同じ選択行の範囲内でCURRVALを参照すると、子側に同一IDを紐付けられます。
-- 親ordersと子order_itemsを同時に作成(行あたり1件の簡略例)
INSERT ALL
INTO orders(id, customer_id, order_dt)
VALUES (orders_seq.NEXTVAL, customer_id, order_dt)
INTO order_items(order_id, item_id, qty, price)
VALUES (orders_seq.CURRVAL , item_id, qty, price)
SELECT customer_id, order_dt, item_id, qty, price
FROM order_staging
WHERE status = 'READY';
1対多の子明細を同時に展開したい場合は、WITH句や集合関数で整形したサブクエリを用意し、1親行ごとに子行をUNION ALLで連結するなど、ソース側を「親子が同じ選択行に並ぶ形」に整える設計が鍵になります。
列変換や派生値の同時適用
挿入先ごとに別の列マッピングや派生計算を同時適用できる点もメリットです。同じソースから、正規化テーブルと集計テーブルの双方へ一括投入する、といったETLパターンを素直に表現できます。
INSERT ALL
INTO fact_sales(id, cust_id, prod_id, qty, amt, sale_dt)
VALUES(id, cust_id, prod_id, qty, amt, sale_dt)
INTO agg_daily_sales(sale_dt, total_amt)
VALUES(sale_dt, amt)
SELECT id, cust_id, prod_id, qty, amt, TRUNC(sale_time) AS sale_dt
FROM landing_sales
WHERE load_flag = 'Y';
エラーログとの併用:DBMS_ERRLOG.LOG_ERRORS
一括投入中の一部行だけが制約違反などで失敗しても処理全体を止めたくない場合は、エラーログ機能を併用します。あらかじめERR$_テーブルを作成し、INSERT ALL の各 INTO 句の末尾に LOG ERRORS INTO … REJECT LIMIT … を付けると、失敗行の詳細を別表に退避しながら処理を継続できます。
BEGIN
DBMS_ERRLOG.CREATE_ERROR_LOG('SALES_MAIN'); -- 例:ERR$_SALES_MAIN を作成
DBMS_ERRLOG.CREATE_ERROR_LOG('SALES_ARCHIVE');
INSERT ALL
INTO sales_main (id, amount, sale_dt) VALUES (id, amount, sale_dt)
LOG ERRORS INTO err$_sales_main REJECT LIMIT UNLIMITED
INTO sales_archive(id, amount, sale_dt) VALUES (id, amount, sale_dt)
LOG ERRORS INTO err$_sales_archive REJECT LIMIT UNLIMITED
SELECT id, amount, sale_dt
FROM staging_sales;
COMMIT;
END;
/
投入後にERR$_テーブルを参照すれば、どの行がどの制約で落ちたかを即時確認できます。
WITH句やマテリアライズドビュー・ヒントでの最適化
ソースデータの前処理が複雑な場合はWITH句で段階的に整形し、最終SELECTでINSERT ALLに流し込みます。中間結果が重い場合は/*+ MATERIALIZE */ヒント(利用可否はバージョン依存)や一時表で一度固めると、同じ計算を複数回繰り返さずに済みます。一度のスキャンで複数表へ書く利点を最大化するため、ソース側SELECTの列とフィルタを必要最小限に絞るのが実務上のコツです。
制約と注意点
RETURNING句はマルチテーブルINSERTでは使えません。挿入後のキー回収が必要なら、シーケンスを明示的に使うか、別途キー一覧をソース側に持たせておきます。トリガーやデフォルト値は挿入先テーブルごとに通常通り発火しますが、複数表で副作用が重なるとデバッグが難しくなるため、業務ロジックは可能な限りSQLに閉じ、トリガーは監査や補助用途に限定するのが安全です。大規模投入ではREDO/UNDOとロック競合の増大に留意し、パーティション分割やNOLOGGING(再生成可能データに限る)などの選択肢も検討します。
まとめ
マルチテーブルINSERTは「一度読むだけで複数に書く」「条件で仕分ける」「親子を同時に作る」というETLに強い表現力と性能上の利点を持ちます。PL/SQLからは通常のINSERT同様に実行でき、シーケンスのNEXTVAL/CURRVALやDBMS_ERRLOGのエラーログと組み合わせれば、堅牢で高速な投入パイプラインをシンプルに構築できます。ALL と FIRST の違い、RETURNING不可といった制約、トランザクション設計とリソース管理を押さえつつ、ソースSELECTの最適化とバッチ単位のコミットで現場適用を安定させましょう。