Oracleで複数データを一括INSERTする方法(複数行/INSERT ALL)

はい、承知いたしました。Oracleデータベースにおける複数データの一括INSERT方法、特に複数行VALUES句とINSERT ALLに焦点を当てつつ、その他の効率的な手法も含めて詳細に解説する記事を作成します。約5000語を目指し、技術的な詳細、使用例、パフォーマンスに関する考慮事項、エラー処理などを網羅します。

以下に記事の内容を直接記述します。


Oracleにおける複数データの一括INSERT方法(複数行 / INSERT ALL)の詳細解説

データベースへのデータ挿入は、アプリケーションの基本的な操作の一つです。しかし、大量のデータを一件ずつ繰り返しINSERTする処理は、非常に効率が悪く、システムのパフォーマンスを著しく低下させる原因となります。Oracleデータベースでは、このような大量データ挿入のシナリオに対応するために、様々な効率的な一括INSERT(バルクインサート)手法を提供しています。

この記事では、Oracleにおけるバルクインサートの基本的な考え方から、特にSQLレベルで手軽に利用できる「複数行VALUES句」や「INSERT ALL」ステートメントに焦点を当て、その詳細な構文、使用例、メリット・デメリットを解説します。さらに、より大規模なデータロードに適したPL/SQLのFORALLステートメント、SQL*Loader、外部表といった手法も紹介し、それぞれの使い分けやパフォーマンスを最大化するためのチューニングポイントについても深く掘り下げていきます。

なぜバルクインサートが必要なのか? 単一行INSERTの非効率性

まず、なぜ一件ずつのINSERTが非効率なのかを理解することから始めましょう。

一般的な単一行INSERT文は以下のようになります。

sql
INSERT INTO target_table (column1, column2, ...)
VALUES (value1, value2, ...);

このステートメントを大量のデータに対して数千回、数万回と繰り返す場合、以下のようなオーバーヘッドが発生します。

  1. ネットワークラウンドトリップ: クライアントアプリケーションからデータベースサーバーへ、INSERT文一つごとにネットワーク通信が発生します。これは非常に大きなオーバーヘッドです。
  2. SQL解析(パース): データベースサーバーは、受け取ったSQL文を毎回解析し、実行計画を生成する必要があります。文が同じでも、パラメータ化されていない場合(リテラル値が埋め込まれている場合など)は、ソフトパースが発生し、そのたびにCPUリソースを消費します。
  3. 実行計画の生成: 解析されたSQL文に対して、最適なデータアクセス経路(実行計画)が生成されます。これもコストのかかる処理です。
  4. SQLエンジンの実行: SQL文が実行され、データが挿入されます。
  5. REDOログの生成: 各INSERT操作はトランザクションの一部として記録され、障害回復のためのREDOログが生成されます。単一行INSERTでは、ログ生成のメタデータ部分のオーバーヘッドが無視できません。
  6. UNDOの生成: ロールバックに備えて、変更前の状態を記録するUNDO情報が生成されます。これもトランザクションごとの管理オーバーヘッドがあります。
  7. ラッチとロック: データ挿入先のテーブルやインデックス、バッファキャッシュに対するラッチやロックの獲得・解放処理が発生します。一件ごとの処理では、これらの細かい制御がボトルネックになり得ます。
  8. コミットのオーバーヘッド: 各INSERT文の後にコミットを行う場合、コミット自体にもディスク書き込みなどのコストがかかります。まとめてコミットする場合でも、上記のオーバーヘッドは残ります。

これらのオーバーヘッドは、一件あたりの処理コストは小さくても、データ量が膨大になると累積され、無視できない実行時間となります。バルクインサートは、これらのオーバーヘッドを最小限に抑えることを目的としています。例えば、一度のネットワークラウンドトリップで複数の行を処理したり、SQL解析や実行計画の生成を一度にまとめたりすることで、効率を劇的に向上させます。

基本的なバルクインサート手法

Oracleで提供されるバルクインサート手法はいくつかありますが、大きく分けて以下のカテゴリーに分類できます。

  1. SQLステートメントによる方法:
    • 複数行VALUES
    • INSERT ALLステートメント(条件付き/無条件)
    • INSERT ... SELECTステートメント
    • CREATE TABLE AS SELECT (CTAS)
  2. PL/SQLによる方法:
    • FORALLステートメントとコレクション(BULK COLLECTと組み合わせて使用)
  3. ユーティリティ/外部ツールによる方法:
    • SQL*Loader
    • 外部表 (External Tables) を利用した INSERT ... SELECT
    • Data Pump

本記事では特に「複数行VALUES句」と「INSERT ALL」に焦点を当て、SQLステートメントで手軽に使えるこれら二つの手法を詳しく解説します。

1. 複数行VALUES

Oracle 11g R1から、単一のINSERTステートメントで複数の行を挿入するために、VALUES句を複数指定できるようになりました。これは最もシンプルで直感的なバルクインサートの手法です。

構文:

sql
INSERT INTO table_name (column1, column2, ...)
VALUES
(value1_row1, value2_row1, ...),
(value1_row2, value2_row2, ...),
...
(value1_rown, value2_rown, ...);

使用例:

仮にemployeesテーブルに新しい従業員データを複数件追加したい場合。

“`sql
— テーブル定義例
— CREATE TABLE employees (
— employee_id NUMBER PRIMARY KEY,
— first_name VARCHAR2(50),
— last_name VARCHAR2(50),
— hire_date DATE,
— salary NUMBER
— );

INSERT INTO employees (employee_id, first_name, last_name, hire_date, salary)
VALUES
(1001, ‘山田’, ‘太郎’, DATE ‘2023-04-01’, 5000),
(1002, ‘田中’, ‘花子’, DATE ‘2023-04-15’, 5500),
(1003, ‘佐藤’, ‘一郎’, DATE ‘2023-05-01’, 5200),
(1004, ‘伊藤’, ‘恵子’, DATE ‘2023-05-10’, 6000);

COMMIT; — または後でまとめてコミット
“`

メリット:

  • 構文がシンプルで分かりやすい。
  • 単一のSQLステートメントとして実行されるため、ネットワークラウンドトリップが一度で済む。
  • SQL解析や実行計画生成のオーバーヘッドが一度で済む。

デメリット:

  • 挿入できる行数に実質的な上限がある。VALUES句が増えるとSQLステートメント自体が非常に長くなり、パース処理や共有プールのメモリ消費に影響を与える可能性がある。通常は数十行から数百行程度までが現実的な範囲とされます。数千、数万といった行数の挿入には適しません。
  • 挿入するデータがどこか別の場所(ファイルや別のテーブル)にある場合、動的にこのSQL文を組み立てる必要があります。

使いどころ:

プログラム中で比較的小数の行(例: フォームからの複数行入力、バッチ処理の最後に残った数件の処理など)をまとめて挿入する場合に有効です。大量データロードの主要な手段として使うべきではありません。

2. INSERT ALLステートメント

INSERT ALLステートメントは、単一のSELECT文から取得したデータを、一つまたは複数のテーブルに挿入するための強力な機能です。ソースデータを一度だけ読み込むという特性から、特にデータを複数のテーブルに振り分けたり、同じデータを複数のテーブルにコピーしたりする場合に高いパフォーマンスを発揮します。

INSERT ALLには、大きく分けて「無条件INSERT ALL」と「条件付きINSERT ALL」の2種類があります。

2.1. 無条件INSERT ALL (Unconditional INSERT ALL)

ソースSELECTの結果セットの全ての行を、指定された全てのINTO句に対応するテーブルに挿入します。データを複数のテーブルにコピーしたい場合に便利です。

構文:

sql
INSERT ALL
INTO target_table_1 (column1, column2, ...) VALUES (value1, value2, ...)
INTO target_table_2 (columnA, columnB, ...) VALUES (valueA, valueB, ...)
...
SELECT
source_column1, source_column2, ...
FROM
source_table
WHERE
...;

VALUES句に指定する値は、SELECTリストで指定されたカラムのエイリアスや位置、あるいは定数などを指定します。SELECTリストのカラムとINTO句のカラムは、データ型互換性があれば、名前や順序が異なっていても構いません。

使用例:

employeesテーブルから特定の条件に合う従業員データを、current_employeesテーブルとemployee_logテーブルの両方にコピーする場合を考えます。

“`sql
— テーブル定義例
— CREATE TABLE current_employees (
— emp_id NUMBER PRIMARY KEY,
— name VARCHAR2(100),
— hire_date DATE
— );

— CREATE TABLE employee_log (
— log_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, — 自動採番
— emp_id NUMBER,
— log_date TIMESTAMP,
— event_desc VARCHAR2(100)
— );

INSERT ALL
INTO current_employees (emp_id, name, hire_date)
VALUES (employee_id, first_name || ‘ ‘ || last_name, hire_date) — VALUES句でSELECTリストのカラムを使用
INTO employee_log (emp_id, log_date, event_desc)
VALUES (employee_id, SYSTIMESTAMP, ‘データコピー完了’) — 定数やSYSTIMESTAMPも使用可能
SELECT
employee_id, first_name, last_name, hire_date
FROM
employees
WHERE
status = ‘Active’;

COMMIT;
“`

この例では、employeesテーブルからstatus = 'Active'の行が選択され、それぞれの行に対して、current_employeesテーブルへの挿入とemployee_logテーブルへの挿入が実行されます。ソースデータを一度だけ読み込み、複数の挿入操作をまとめて行うため、効率的です。

メリット:

  • ソースデータを一度だけ読み込めばよいため、繰り返しINSERT ... SELECTを実行するよりも効率が良い。
  • 複数のテーブルへのデータ挿入を単一の原子的なステートメントとして実行できる。

デメリット:

  • ソースSELECTの結果セットの全ての行が、指定された全てのINTO句の対象となるため、行ごとに挿入先テーブルを振り分けたい場合には適さない(その場合は条件付きINSERT ALLを使用)。

2.2. 条件付きINSERT ALL (Conditional INSERT ALL)

ソースSELECTの結果セットの各行に対して、指定された条件 (WHEN句) を評価し、条件を満たしたINTO句に対応するテーブルにのみデータを挿入します。データを複数のテーブルに振り分けたい場合に非常に便利です。

条件付きINSERT ALLには、さらに「INSERT ALL」(全ての条件を評価)と「INSERT FIRST」(最初の条件を満たした時点で評価を停止)の2種類があります。

INSERT ALL (条件付き – すべての条件を評価)

ソースSELECTの各行に対して、全てのWHEN句の条件を評価します。ある行が複数のWHEN句の条件を満たす場合、その行は条件を満たした全てのINTO句に対応するテーブルに挿入されます。

構文:

sql
INSERT ALL
WHEN condition_1 THEN
INTO target_table_1 (column1, ...) VALUES (value1, ...)
WHEN condition_2 THEN
INTO target_table_2 (columnA, ...) VALUES (valueA, ...)
...
[ELSE
INTO default_table (columnX, ...) VALUES (valueX, ...)]
SELECT
source_column1, source_column2, ...
FROM
source_table
WHERE
...;

condition_nは、ソースSELECTで選択されたカラムや定数を使用した任意の条件式です。ELSE句はオプションで、どのWHEN句の条件も満たさなかった行に対する挿入先を指定します。

使用例:

order_detailsテーブルのデータを、注文金額や商品カテゴリーによって異なるテーブル(例: high_value_orders, low_value_orders, electronics_orders)に振り分けて挿入する場合を考えます。

“`sql
— テーブル定義例
— CREATE TABLE order_details (
— order_id NUMBER,
— item_id NUMBER,
— amount NUMBER,
— category VARCHAR2(50)
— );

— CREATE TABLE high_value_orders (…) — order_detailsと同じ構造+α
— CREATE TABLE low_value_orders (…) — order_detailsと同じ構造+α
— CREATE TABLE electronics_orders (…) — order_detailsと同じ構造+α
— CREATE TABLE other_orders (…) — order_detailsと同じ構造+α

INSERT ALL
— 金額が10000を超える場合
WHEN amount > 10000 THEN
INTO high_value_orders (order_id, item_id, amount, category) VALUES (order_id, item_id, amount, category)
— 金額が10000以下の場合 (high_value_ordersの条件も満たしうる)
WHEN amount <= 10000 THEN
INTO low_value_orders (order_id, item_id, amount, category) VALUES (order_id, item_id, amount, category)
— カテゴリーがElectronicsの場合 (金額に関係なく挿入)
WHEN category = ‘Electronics’ THEN
INTO electronics_orders (order_id, item_id, amount, category) VALUES (order_id, item_id, amount, category)
— 上記いずれの条件も満たさない場合
ELSE
INTO other_orders (order_id, item_id, amount, category) VALUES (order_id, item_id, amount, category)
SELECT
order_id, item_id, amount, category
FROM
order_details_staging — 元データがあるステージングテーブル
WHERE
process_flag = ‘N’; — 未処理の行を選択
“`

この例では、金額12000でカテゴリーが’Electronics’の行は、high_value_orderselectronics_ordersの両方に挿入されます。金額5000でカテゴリーが’Books’の行は、low_value_ordersother_ordersの両方に挿入されます。

INSERT FIRST (条件付き – 最初の条件のみ評価)

ソースSELECTの各行に対して、WHEN句の条件を記述された順に評価します。条件を満たした最初のWHEN句に対応するテーブルにのみデータを挿入し、その行に対する評価はそこで終了します。これは、行を排他的に一つのテーブルに振り分けたい場合に適しています。

構文:

sql
INSERT FIRST
WHEN condition_1 THEN
INTO target_table_1 (column1, ...) VALUES (value1, ...)
WHEN condition_2 THEN
INTO target_table_2 (columnA, ...) VALUES (valueA, ...)
...
[ELSE
INTO default_table (columnX, ...) VALUES (valueX, ...)]
SELECT
source_column1, source_column2, ...
FROM
source_table
WHERE
...;

使用例:

上記の注文データの振り分け例を、今度は行が いずれか一つ のテーブルにのみ挿入されるように変更します。

sql
INSERT FIRST
-- 金額が10000を超える場合
WHEN amount > 10000 THEN
INTO high_value_orders (order_id, item_id, amount, category) VALUES (order_id, item_id, amount, category)
-- カテゴリーがElectronicsの場合 (金額が10000以下でカテゴリーがElectronicsの行がここに来る)
WHEN category = 'Electronics' THEN
INTO electronics_orders (order_id, item_id, amount, category) VALUES (order_id, item_id, amount, category)
-- 金額が10000以下の場合 (かつカテゴリーがElectronicsでない行がここに来る)
WHEN amount <= 10000 THEN -- (amount > 10000 は上のWHENで処理済みのため実質 <= 10000)
INTO low_value_orders (order_id, item_id, amount, category) VALUES (order_id, item_id, amount, category)
-- 上記いずれの条件も満たさない場合 (ありえないケースだが念のため)
ELSE
INTO other_orders (order_id, item_id, amount, category) VALUES (order_id, item_id, amount, category)
SELECT
order_id, item_id, amount, category
FROM
order_details_staging
WHERE
process_flag = 'N';

この例では、金額12000でカテゴリーが’Electronics’の行は、最初のWHEN amount > 10000 THENの条件を満たし、high_value_ordersに挿入された後、その行の評価は終了します(electronics_ordersには挿入されない)。金額5000でカテゴリーが’Electronics’の行は、最初の条件を満たしませんが、二番目のWHEN category = 'Electronics' THENを満たし、electronics_ordersに挿入された後、評価が終了します(low_value_ordersには挿入されない)。金額5000でカテゴリーが’Books’の行は、最初の二つの条件を満たさず、三番目のWHEN amount <= 10000 THENを満たし、low_value_ordersに挿入されます。

INSERT FIRSTを使用する場合、WHEN句の記述順序が結果に影響するため、注意が必要です。より限定的な条件や優先したい条件を先に記述するのが一般的です。

条件付きINSERT ALL (INSERT ALL vs INSERT FIRST) のメリット:

  • ソースデータを一度だけ読み込み、複雑な振り分け処理をSQLステートメント内で効率的に実行できる。
  • プログラム側でデータをループして条件分岐し、複数回INSERT文を発行するよりも、はるかに高性能。

条件付きINSERT ALL (INSERT ALL vs INSERT FIRST) のデメリット:

  • INSERT ALL(すべての条件評価)とINSERT FIRST(最初の条件のみ評価)の違いを理解し、適切に使い分ける必要がある。
  • SELECTリストで取得したカラムは、すべてのINTO句で使用可能でなければならない(ただし、実際に使用しないカラムを含めても構わない)。
  • RETURNING句は使用できない。
  • エラーが発生した場合、ステートメント全体がロールバックされる(部分的な成功は許容されない)。DMLエラーログ機能 (LOG ERRORS) を併用することで、エラー行をスキップして正常行のみを挿入することは可能。

使いどころ:

  • 単一のソースから取得したデータを、条件に基づいて複数のテーブルに振り分けたい場合。
  • データを複数のサマリーテーブルやログテーブルに同時に挿入したい場合。
  • ETL処理におけるデータ整形・分配ステップ。

3. INSERT ... SELECTステートメント

これも基本的なバルクインサート手法であり、あるテーブル(またはビュー、副問い合わせ)からデータを取得し、別のテーブルにまとめて挿入します。INSERT ALLの最も単純なケース(一つのINTO句を持つ無条件INSERT ALL)と似ていますが、より一般的で多くのデータベースシステムでサポートされています。

構文:

sql
INSERT INTO target_table (column1, column2, ...)
SELECT source_column1, source_column2, ...
FROM source_table
WHERE ...;

使用例:

staging_tableからproduction_tableへデータを移動する場合。

“`sql
— テーブル定義は省略

INSERT INTO production_table (col1, col2, col3)
SELECT stg_col1, stg_col2, stg_col3
FROM staging_table
WHERE status = ‘Validated’;

COMMIT;
“`

メリット:

  • 構文がシンプルで分かりやすい。
  • 大量のデータをテーブル間で移動/コピーする際に効率的。
  • WHERE句や結合を使用して、挿入するデータを柔軟にフィルタリング、変換できる。
  • Oracle独自の拡張機能として、/*+ APPEND */ ヒントを使用することで、さらに高速な挿入が可能になる場合がある(後述)。

デメリット:

  • 一つのINSERT ... SELECTでは、データを複数の異なるテーブルに直接同時に挿入することはできない(複数のテーブルに挿入したい場合は、INSERT ALLを使用するか、複数のINSERT ... SELECTを実行する必要がある)。
  • ソースデータに対する複雑な条件分岐(例: この行はテーブルA、あの行はテーブルB)を単一のステートメント内で行うことはできない。

使いどころ:

  • ステージングテーブルから本番テーブルへのデータ投入。
  • アーカイブテーブルへのデータ移動。
  • 変換を伴うデータコピー。

INSERT /*+ APPEND */ SELECT ヒント

INSERT ... SELECTにおいて、/*+ APPEND */ ヒントを使用すると、通常のINSERT処理(バッファキャッシュを介した処理)をバイパスし、データを直接データファイルに書き込みます。これにより、特に既存のデータがないテーブルの末尾に大量データを挿入する場合や、テーブル全体を新しいデータで置き換える場合に、劇的なパフォーマンス向上をもたらす可能性があります。

構文:

sql
INSERT /*+ APPEND */ INTO target_table (column1, ...)
SELECT source_column1, ...
FROM source_table
WHERE ...;

注意点:

  • このヒントを使用すると、挿入されるデータに対して排他ロックがかかります。他のセッションからのアクセスが制限される場合があります。
  • デフォルトではNOLOGGINGモードで実行され、REDOログの生成が最小限に抑えられます。これにより高速化されますが、ロード中にインスタンス障害が発生した場合、最新のデータまで回復できない可能性があります。完全な障害回復が必要な場合は、テーブルをLOGGINGモードに設定するか、APPENDヒントと同時にFORCE LOGGING指定を使用する必要があります(ただし、この場合パフォーマンスメリットは低下します)。
  • 挿入先のテーブルにデータが既存する場合、データの挿入位置は必ずテーブルの最後尾になります(フリーリストを使用せず、ハイウォーターマークより後ろに書き込まれます)。

使いどころ:

  • 既存のテーブルをほぼ空にしてから大量データを再ロードする場合。
  • パーティションテーブルの新しいパーティションにデータをロードする場合。
  • 一時的な作業テーブルに大量データを高速に格納する場合。

4. PL/SQLとFORALLステートメント

SQLステートメントだけでは対応できない複雑なロジックや、プログラム側で保持しているデータを効率的に挿入したい場合、PL/SQLのFORALLステートメントが非常に強力です。

前述の単一行INSERTの非効率性は、特にPL/SQLのループ内でINSERT文を繰り返し実行する際に顕著になります。これは、PL/SQLエンジンとSQLエンジンの間で繰り返しコンテキストスイッチが発生するためです。

sql
-- 非効率な例: PL/SQLループ内での単一行INSERT
BEGIN
FOR rec IN (SELECT ... FROM source_table) LOOP
-- ループごとにSQLエンジンへのコンテキストスイッチとINSERT処理
INSERT INTO target_table (col1, col2) VALUES (rec.col1, rec.col2);
END LOOP;
-- COMMIT; -- コミットはまとめて行うとしても、INSERT処理ごとのオーバーヘッドは大きい
END;
/

FORALLステートメントは、このようなコンテキストスイッチのオーバーヘッドを削減するために導入されました。これは、PL/SQLコレクション(配列のようなもの)に格納された複数の要素に対して、単一のDMLステートメント(INSERT, UPDATE, DELETE, MERGE)を一度に適用するための構造です。

構文:

sql
FORALL index IN collection_index_range
DML_statement_using_collection_elements;

collection_index_rangeは、コレクションの要素の範囲を指定します(例: collection_name.FIRST .. collection_name.LAST, indices of collection_name, values of collection_nameなど)。DML_statement_using_collection_elementsは、コレクションの要素をバインド変数として使用するDMLステートメントです。

FORALLを使用するには、まず挿入したいデータをPL/SQLコレクションにロードする必要があります。これには通常、SELECT ... BULK COLLECT INTO collection_variable 句が使用されます。BULK COLLECTは、複数の行を一度にフェッチしてコレクションに格納するため、カーソルループで一行ずつフェッチするよりも効率的です。

詳細な使用例:

source_data_tableから特定の条件のデータを取得し、加工してからtarget_data_tableに挿入する場合。

“`sql
— コレクション型を定義 (必要に応じて)
— TYPE data_record_type IS RECORD (
— id NUMBER,
— name VARCHAR2(100),
— value NUMBER
— );
— TYPE data_collection_type IS TABLE OF data_record_type INDEX BY PLS_INTEGER;

— あるいは、テーブルの行型に基づくコレクション型
TYPE target_data_coll_t IS TABLE OF target_data_table%ROWTYPE INDEX BY PLS_INTEGER;

— コレクション変数を宣言
l_data_to_insert target_data_coll_t;

— 挿入対象データの取得とコレクションへの格納 (BULK COLLECTを使用)
BEGIN
— バルクコレクトのサイズ制限を指定することで、メモリ消費を抑えつつ効率的なフェッチが可能
— 必要に応じて、ループ内でBULK COLLECT + FORALL を繰り返す (バッチ処理)
SELECT
id,
UPPER(name), — データ加工例
value * 1.1 — データ加工例
BULK COLLECT INTO l_data_to_insert
FROM
source_data_table
WHERE
process_flag = ‘N’ AND status = ‘Approved’
LIMIT 1000; — 例: 1000件ずつ処理

-- コレクションが空でなければFORALLで挿入
IF l_data_to_insert.COUNT > 0 THEN
    -- FORALLを使用してコレクションのデータを一括挿入
    FORALL i IN l_data_to_insert.FIRST .. l_data_to_insert.LAST
        INSERT INTO target_data_table (id, name, value)
        VALUES (l_data_to_insert(i).id, l_data_to_insert(i).name, l_data_to_insert(i).value);

    -- コミットはFORALLの外で行う
    -- COMMIT; -- またはより大きなバッチ単位でコミット
END IF;

-- 必要に応じて、LIMIT句で処理した続きをループで回す
-- ... (カーソルとLOOP, EXIT WHEN ... LIMIT ...) ...

COMMIT; -- 最終的なコミット

EXCEPTION
WHEN OTHERS THEN
ROLLBACK; — エラー時はロールバック
RAISE; — 例外を再スロー
END;
/
“`

FORALL + BULK COLLECT のパフォーマンス:

FORALLは、コレクション全体を一度のデータベース呼び出しで処理しようとします(内部的には、コレクションの各要素がバインド変数として渡され、DML文が繰り返し実行されるように見えますが、オーバーヘッドは最小化されています)。BULK COLLECTは、複数の行を一度のフェッチでコレクションに格納します。この二つを組み合わせることで、PL/SQLとSQLエンジンの間のコンテキストスイッチを劇的に削減し、大量データ処理のパフォーマンスを大幅に向上させることができます。

バッチサイズとコミット:

BULK COLLECTで一度にコレクションに格納する行数(LIMIT句)と、FORALLを実行しコミットする頻度(バッチサイズ)は、パフォーマンスとリソース使用量(UNDO、REDO、メモリ)に大きな影響を与えます。

  • バッチサイズが大きい: コンテキストスイッチの回数は減り、処理時間は短縮される傾向がありますが、メモリ使用量が増加し、トランザクションサイズが大きくなるため、ロールバックが遅くなったり、UNDO領域を圧迫したりする可能性があります。また、エラー発生時の影響範囲が広くなります。
  • バッチサイズが小さい: メモリ使用量は少なく、エラー発生時の影響範囲は限定的ですが、コンテキストスイッチの回数が増え、処理時間が増加します。

適切なバッチサイズは、処理するデータ量、システムのメモリ、UNDO領域、REDO生成速度などを考慮してテストしながら決定する必要があります。一般的には、数千から数万行程度が多くのシステムで良いバランスとされています。

エラー処理 (SAVE EXCEPTIONS):

デフォルトでは、FORALLステートメントの実行中に途中でエラーが発生すると、ステートメント全体が失敗し、それまでに処理された行も含めてロールバックされます。しかし、SAVE EXCEPTIONS句を使用すると、エラーが発生しても処理を続行し、全ての例外情報を収集することができます。FORALLの実行が完了した後、SQL%BULK_EXCEPTIONSという暗黙のカーソル属性を使って、発生したエラーの詳細(どのコレクション要素で、どのようなエラーコードか)を取得し、個別にエラー処理を行うことができます。

“`sql
— FORALLとSAVE EXCEPTIONSによるエラー処理例
BEGIN
— … BULK COLLECT …

IF l_data_to_insert.COUNT > 0 THEN
    BEGIN
        FORALL i IN l_data_to_insert.FIRST .. l_data_to_insert.LAST SAVE EXCEPTIONS
            INSERT INTO target_data_table (id, name, value)
            VALUES (l_data_to_insert(i).id, l_data_to_insert(i).name, l_data_to_insert(i).value);

    EXCEPTION
        WHEN OTHERS THEN -- FORALL with SAVE EXCEPTIONS raises a specific exception
            DBMS_OUTPUT.PUT_LINE('FORALLで例外が発生しました。');
            DBMS_OUTPUT.PUT_LINE('エラー件数: ' || SQL%BULK_EXCEPTIONS.COUNT);

            -- 発生したエラーの詳細をループで取得
            FOR j IN 1 .. SQL%BULK_EXCEPTIONS.COUNT LOOP
                DBMS_OUTPUT.PUT_LINE(
                    'インデックス: ' || SQL%BULK_EXCEPTIONS(j).ERROR_INDEX ||
                    ', エラーコード: ' || SQL%BULK_EXCEPTIONS(j).ERROR_CODE ||
                    ', エラーメッセージ: ' || SQLERRM(-SQL%BULK_EXCEPTIONS(j).ERROR_CODE)
                );
                -- エラーになった要素のデータや、エラーとなった理由に応じた処理を行う
                -- 例: エラー行を別のテーブルに記録する
                -- INSERT INTO error_log_table ... VALUES (l_data_to_insert(SQL%BULK_EXCEPTIONS(j).ERROR_INDEX)...);
            END LOOP;

            -- 全体のトランザクションをコミットまたはロールバックするかは、要件による
            -- エラー行をログに記録してコミットする場合:
            -- COMMIT;
            -- 全体をロールバックする場合:
            ROLLBACK;
            -- あるいは、エラーログテーブルへのINSERTは別途コミットし、残りはロールバックなど...

            -- 必要に応じて、処理を中断するか継続するかを判断
            -- RAISE; -- 中断する場合
    END;
END IF;

-- 正常に処理された行をコミット(SAVE EXCEPTIONSを使用し、かつロールバックしなかった場合)
-- COMMIT;

EXCEPTION
WHEN OTHERS THEN — FORALL以外のエラーハンドリング
ROLLBACK;
RAISE;
END;
/
“`

SAVE EXCEPTIONSは、バルク処理においてエラーが発生した行をスキップし、正常な行だけを処理続行したい場合に非常に役立ちます。

FORALLのメリット:

  • 大量データをプログラム側で処理・加工してから効率的にデータベースに挿入できる。
  • PL/SQLとSQLエンジンの間のコンテキストスイッチを最小限に抑え、高いパフォーマンスを発揮する。
  • BULK COLLECTと組み合わせることで、フェッチとインサートの両方を効率化できる。
  • SAVE EXCEPTIONSによるきめ細やかなエラー処理が可能。

FORALLのデメリット:

  • PL/SQLの知識が必要。
  • データを一度コレクションにロードするため、大量のデータを一度に処理しようとするとメモリを大量に消費する可能性がある(バッチ処理で対応)。
  • SQLステートメント単体よりもコード量が多くなり、記述がやや複雑になる。

使いどころ:

  • アプリケーションロジックやバッチ処理の中で、メモリ上のデータや、加工が必要な大量データをデータベースに挿入する場合。
  • SQL*Loaderや外部表を使うほどではないが、INSERT ALLでは実現できない複雑なロジックが必要な場合。
  • エラー発生時に処理を中断せず、エラー行だけをスキップして後で調査したい場合。

その他のバルクロード手法(概要)

記事の主題である複数行VALUES句とINSERT ALL、そしてPL/SQLのFORALLについて詳しく解説しましたが、Oracleにはさらに大規模なデータロードに特化した強力なツールがあります。これらについても簡単に触れておきます。

5. SQL*Loader

SQL*Loaderは、オペレーティングシステム上の様々な形式のデータファイル(CSV、固定長、区切り文字など)をOracleデータベースのテーブルに高速にロードするための専用ユーティリティです。

  • 特徴:
    • 非常に高いロードパフォーマンス。
    • 制御ファイル(.ctl)でロード方法、データフォーマット、データの選択・加工方法、エラー処理などを細かく指定できる。
    • 「ダイレクトパスロード」モード(/*+ APPEND */ ヒントに似た内部動作)を使用すると、通常のロードよりもさらに高速になる。
    • エラー行や破棄された行を別のファイルに出力できる。
  • 使いどころ:
    • 外部ファイルからデータベースへの大量データ初期ロードや定期的なバッチロード。
    • データの検証や簡単な変換をロードプロセスで行いたい場合。

6. 外部表 (External Tables)

外部表は、オペレーティングシステム上のデータファイルを、あたかもデータベース内の読み取り専用表であるかのように扱うことができる機能です。データファイルの内容を直接問い合わせたり、結合したり、INSERT ... SELECTのソースとして利用したりできます。

  • 特徴:
    • データファイルをデータベースにロードする必要がない(読み込み時のみファイルにアクセス)。
    • SQLを使用してファイル内のデータをクエリできる。
    • 外部表をソースとしたINSERT ... SELECTで、ファイルからテーブルへのロードを効率的に行える。SQL*Loader制御ファイルに似たアクセスドライバパラメータでファイルフォーマットを指定する。
  • 使いどころ:
    • 定期的に外部ファイルからデータをロードする必要があるが、SQL*Loader制御ファイルの管理が煩雑な場合。
    • 外部ファイルをデータベース内の他の表と結合して処理したい場合。
    • ファイル内容を一時的に確認したり、簡単な分析を行ったりしたい場合。

バルクインサートのパフォーマンスチューニング共通事項

どのバルクインサート手法を選択しても、以下の点に注意することで、パフォーマンスをさらに向上させることができます。

  1. コミット頻度: トランザクションサイズが大きすぎると、UNDO生成、ロールバックの可能性、ロックの保持時間が増加します。小さすぎると、コミットごとのオーバーヘッドが増加します。適切なバッチサイズでコミットを行うことが重要です。
  2. インデックス: B*Treeインデックスは、データ挿入時にメンテナンス(更新、分割)が発生するため、バルクインサートの大きなボトルネックとなります。大量ロードの前に対象テーブルのインデックスをDROPまたはUNUSABLEにし、ロード完了後に再作成またはリビルドすることで、ロード時間を劇的に短縮できる場合があります(ただし、ロード中のクエリ性能に影響)。Bitmapインデックスは、一括更新に適した構造を持っているため、DROP/REBUILDが常に最適とは限りません。
  3. 制約とトリガー: CHECK制約、FOREIGN KEY制約、UNIQUE制約、PRIMARY KEY制約、トリガーも、データ挿入ごとに評価・実行されるため、オーバーヘッドとなります。ロードの前に対象の制約(特にNOT NULL以外のもの)やトリガーを無効化し、ロード完了後に有効化(NOT VALIDATEDオプション付きで高速有効化も可能)することで、パフォーマンスを改善できます。
  4. ロギング (NOLOGGING): INSERT /*+ APPEND */ヒントやダイレクトパスロード(SQL*Loader)は、デフォルトでNOLOGGINGモードになります。テーブル自体をNOLOGGINGに設定することも可能です。これによりREDOログの生成が最小限に抑えられ、ディスクI/Oが削減されて高速化されますが、ロード中のインスタンス障害からの回復が限定的になるリスクがあります。システムのRPO (Recovery Point Objective) 要件を考慮して判断が必要です。
  5. 並列処理 (PARALLEL): INSERT ... SELECTステートメントに/*+ PARALLEL */ヒントを使用したり、テーブルをパラレルモードに設定したりすることで、複数のプロセスが同時に挿入処理を行い、ロード時間を短縮できる場合があります。ALTER SESSION ENABLE PARALLEL DML; の実行が必要です。
  6. UNDO領域とREDOログ: 大量のデータを一度に挿入すると、UNDOログとREDOログが大量に生成されます。UNDO表領域が不足したり、REDOログスイッチが頻繁に発生したりすると、パフォーマンスが低下したり、処理が失敗したりします。適切なUNDO表領域サイズやREDOログファイルサイズ、数を確認・調整することが重要です。
  7. DMLエラーロギング (LOG ERRORS): SQLステートメント(INSERT, UPDATE, MERGE)には、エラーが発生した行をスキップしてログテーブルに記録する機能があります。LOG ERRORS句を使用することで、ロード処理自体を中断することなく、エラー行だけを後で調査・再処理できます。これはINSERT ALL, INSERT ... SELECTでも有効です。

“`sql
— LOG ERRORS 句の使用例
INSERT INTO target_table (col1, col2)
SELECT source_col1, source_col2 FROM source_table
WHERE …
LOG ERRORS INTO err$_target_table (‘My_Load_Batch_1’) REJECT LIMIT UNLIMITED; — 無制限にエラーを許容

— エラーログテーブルは事前に作成が必要 (DBMS_ERRLOG.CREATE_ERROR_LOG)
— EXEC DBMS_ERRLOG.CREATE_ERROR_LOG (‘TARGET_TABLE’, ‘ERR$_TARGET_TABLE’);
“`

各手法の比較と使い分け

手法 ソースデータ 柔軟性 パフォーマンス(大規模) 記述の複雑さ 主なユースケース
複数行VALUES プログラム/固定値 シンプルな固定データ 低(少行向け) 比較的小数の固定データの挿入、テストデータ作成
INSERT ALL DB内テーブル等 条件付き振り分け/コピー DB内のデータを複数のテーブルに振り分け/コピー
INSERT FIRST DB内テーブル等 排他的な振り分け DB内のデータを条件により単一テーブルに振り分け
INSERT ... SELECT DB内テーブル等 変換/フィルタリング DB内テーブル間のデータコピー/移動
INSERT /*+ APPEND */ SELECT DB内テーブル等 変換/フィルタリング 非常に高(特定条件) 大量データによるテーブルの再構成、新規パーティションロード
FORALL (PL/SQL) コレクション 高(複雑な加工/ロジック) 非常に高(バッチ処理) PL/SQL内でデータ処理/加工後のバルク挿入、エラー処理
SQL*Loader 外部ファイル ファイルフォーマット処理 非常に高(専用) ツール/制御ファイル 外部ファイルからの大規模データ初期ロード/バッチロード
外部表 + INSERT SELECT 外部ファイル SQLによるファイル参照 外部ファイルからのロード、SQLによるファイルデータ参照

まとめ

Oracleデータベースで大量のデータを効率的に挿入するためには、単一行のINSERT文を繰り返すのではなく、バルクインサート手法を活用することが不可欠です。

  • 比較的小数の固定データやプログラムで生成されたデータには、シンプルで分かりやすい複数行VALUESが手軽です。
  • データベース内の既存データを取得し、複数のテーブルに振り分けたりコピーしたりする場合は、INSERT ALL (INSERT ALL または INSERT FIRST) が最適です。ソースデータを一度だけ読み込む効率性が大きなメリットです。
  • あるテーブルから別のテーブルへデータをまとめてコピー・移動する場合は、基本的なINSERT ... SELECTが便利です。特に大量データで追記ロードを行う場合は、/*+ APPEND */ ヒントの利用を検討価値があります。
  • より複雑なデータ加工やロジックが必要な場合、あるいはプログラム側のデータを扱う場合は、PL/SQLのFORALLBULK COLLECTの組み合わせが最も強力で柔軟な選択肢となります。SAVE EXCEPTIONSによるきめ細やかなエラー処理も可能です。
  • 非常に大規模な外部ファイルからのロードには、専用ユーティリティであるSQL*Loader外部表を利用したINSERT ... SELECTが適しています。

これらの手法は、それぞれ異なる得意分野と特性を持っています。データソースの形式、データ量、必要な加工・振り分けロジック、エラー処理要件などを総合的に考慮し、状況に応じて最適な手法を選択することが、パフォーマンスの高いデータ処理を実現する鍵となります。

実際のシステムでは、これらの手法を組み合わせることもあります。例えば、SQL*Loaderや外部表でデータをステージングテーブルにロードし、その後、PL/SQLのFORALLやSQLのINSERT ALLを使って本番テーブルに投入するといったワークフローが一般的です。

最後に、バルクインサートの性能は、インデックス、制約、ロギング、並列処理、UNDO/REDO管理などの様々な要因に影響されます。実際のデータと環境で十分なテストを行い、最適なチューニングを行うことが、期待されるパフォーマンスを得るためには不可欠です。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール