【Oracle】ORA-12838の原因と解決方法|並列DML・APPEND後に同じ表を読めない時の対処

【Oracle】ORA-12838の原因と解決方法|並列DML・APPEND後に同じ表を読めない時の対処 Oracle

ORA-12838: cannot read/modify an object after modifying it in parallel は、並列DMLやダイレクト・パスINSERTで表を変更したあと、同じトランザクション内でその表を再度SELECT、UPDATE、DELETE、MERGEしようとした時に発生するエラーです。大量ロードや高速INSERTのあとに件数確認、後続UPDATE、同じ表へのMERGEを続けた時に出やすいです。

結論から言うと、並列DMLまたはAPPENDで変更した表は、COMMITまたはROLLBACKするまで同じトランザクション内で再アクセスできないと考えるのが安全です。Oracle公式のエラー説明でも、対処はトランザクションをCOMMITまたはROLLBACKしてから再アクセスすることとされています。並列DMLの基本は Oracle並列実行完全ガイド、APPENDの基本は ダイレクト・パス・インサート完全ガイド もあわせて確認してください。

この記事で整理すること

  • ORA-12838が発生する条件
  • 並列DMLとAPPENDで再現するSQL
  • COMMITで解消する理由
  • APPENDを外す、処理を分ける、一時表を使う対策
  • MERGE、CTAS、Data Pump、PL/SQLバッチでの注意点
  • 本番投入前のチェックリスト
スポンサーリンク

重複確認結果

本番DBで ORA-12838、英語メッセージ、並列DML、APPEND後などを検索しましたが、ORA-12838単独記事は見つかりませんでした。既存には並列実行、APPEND、MERGE、SQLヒント、LOG ERRORSの記事があるため、この記事はそれらをつなぐエラー対処記事として作成しています。

最初に結論:COMMITするまで同じ表に触らない

ORA-12838の一番シンプルな対処は、対象表を変更した直後に COMMIT または ROLLBACK でトランザクションを終わらせることです。その後でSELECTやUPDATEを行えば、エラーは解消します。

すぐ直したい並列DMLまたはAPPEND後に COMMIT してから、件数確認や後続処理を実行します。
COMMITできない同じトランザクションで検証や後続更新が必要なら、APPENDや並列DMLを使わない設計へ変えます。
高速化も必要ステージング表へAPPENDし、COMMIT後に別トランザクションで検証・MERGEする構成に分けます。
バッチで再発する処理単位、COMMIT境界、件数確認SQL、同じ表への後続DMLを棚卸しします。

COMMIT境界の設計は 大量データ処理のコミット頻度とUNDO最適化、PL/SQLバッチ全体の性能設計は PL/SQLパフォーマンス改善プレイブック が関連します。

発生条件の判断フロー

ORA-12838は、対象表に何をした直後なのかを見ると切り分けやすいです。単にSELECTが失敗したように見えても、直前のDMLがAPPENDや並列DMLだった、という流れが多いです。

直前がINSERT /*+ APPEND */ダイレクト・パスINSERT後に同じ表を読んだ可能性が高いです。COMMIT後に確認するか、APPENDを外します。
直前がPARALLEL付きDMLALTER SESSION ENABLE PARALLEL DML が有効な状態でUPDATE、DELETE、MERGEした可能性があります。
直前がMERGEMERGE対象表を同じトランザクションで件数確認・追加更新していないか確認します。
直前がバッチロードロード、検証、反映が同じ表に詰め込まれていないか見ます。ステージング表分離が有効です。

APPENDと並列DMLの切り分け

APPENDと並列DMLは一緒に使われることもありますが、切り分けでは分けて考えると原因を追いやすくなります。APPENDはダイレクト・パスINSERTを狙うヒント、並列DMLはDMLを並列実行するセッション設定またはヒントです。

APPENDだけで起きる形

append-only-check.sql
-- 並列DMLを明示していなくても、APPEND後に同じ表を読むと問題になることがある
INSERT /*+ APPEND */ INTO load_work
SELECT *
FROM load_source;

SELECT COUNT(*)
FROM load_work; -- ORA-12838の可能性

ROLLBACK;

並列DMLで起きる形

parallel-dml-check.sql
ALTER SESSION ENABLE PARALLEL DML;

DELETE /*+ PARALLEL(load_work 4) */ FROM load_work
WHERE batch_id = :batch_id;

SELECT COUNT(*)
FROM load_work; -- ORA-12838の可能性

ROLLBACK;
ALTER SESSION DISABLE PARALLEL DML;

どちらも対処の中心は、同じ表に再アクセスする前にトランザクションを区切ることです。それができない処理では、APPENDや並列DMLを外すか、対象表を分けます。

ORA-12838が起きる典型パターン

ORA-12838は、並列DMLやダイレクト・パスINSERTによって変更された表に、同一トランザクション内で再アクセスした時に起きます。読み取りだけでも対象になります。つまり、INSERT後に SELECT COUNT(*) しただけでも失敗することがあります。

APPEND INSERT後にSELECTする

append-select-ng.sql
INSERT /*+ APPEND */ INTO sales_work
SELECT *
FROM sales_source
WHERE sales_date >= DATE '2026-01-01';

-- 同じトランザクション内で同じ表を読むため ORA-12838
SELECT COUNT(*)
FROM sales_work;

並列DML後にUPDATEする

parallel-dml-update-ng.sql
ALTER SESSION ENABLE PARALLEL DML;

UPDATE /*+ PARALLEL(sales_work 4) */ sales_work
SET load_status = 'DONE'
WHERE load_status = 'NEW';

-- 同じ表を再度変更するため ORA-12838
UPDATE sales_work
SET checked_flag = 'Y'
WHERE load_status = 'DONE';

MERGE後に件数確認する

merge-check-ng.sql
ALTER SESSION ENABLE PARALLEL DML;

MERGE /*+ PARALLEL(t 4) */ INTO customer_summary t
USING customer_summary_stage s
ON (t.customer_id = s.customer_id)
WHEN MATCHED THEN
  UPDATE SET t.total_amount = s.total_amount
WHEN NOT MATCHED THEN
  INSERT (customer_id, total_amount)
  VALUES (s.customer_id, s.total_amount);

-- 同じトランザクション内で対象表を読むと ORA-12838
SELECT COUNT(*)
FROM customer_summary;

MERGE自体の設計は Oracle MERGE文完全ガイドPL/SQL MERGE文でUPSERTを高速・安全に実装 も参考になります。

なぜCOMMITで解消するのか

並列DMLやダイレクト・パスINSERTは、通常のDMLとは異なる内部処理で表を変更します。処理後、同じトランザクション内では対象表に対する読み書きが制限されます。COMMITまたはROLLBACKでトランザクションを終わらせると、その制限が解除されます。

commit-then-read.sql
INSERT /*+ APPEND */ INTO sales_work
SELECT *
FROM sales_source
WHERE sales_date >= DATE '2026-01-01';

COMMIT;

-- COMMIT後なら読める
SELECT COUNT(*)
FROM sales_work;

ただし、COMMITを入れればよい、で終わらせない方が安全です。途中でCOMMITすると、後続処理で失敗した場合に一括ROLLBACKできません。再実行設計、ステータス列、処理ログ、エラー行の隔離まで考える必要があります。

対策1:COMMIT境界を明確に分ける

最も分かりやすい設計は、ロード、検証、反映をトランザクション単位で分けることです。APPENDや並列DMLを使う処理は高速化専用のステップにし、COMMIT後に検証や後続DMLへ進みます。

batch-step-commit.sql
-- Step 1: 高速ロード
INSERT /*+ APPEND */ INTO sales_stage
SELECT *
FROM external_sales;

COMMIT;

-- Step 2: 件数確認
SELECT COUNT(*)
FROM sales_stage;

-- Step 3: 本表へ反映
MERGE INTO sales t
USING sales_stage s
ON (t.sales_id = s.sales_id)
WHEN MATCHED THEN
  UPDATE SET t.amount = s.amount
WHEN NOT MATCHED THEN
  INSERT (sales_id, amount)
  VALUES (s.sales_id, s.amount);

COMMIT;

この形なら、APPEND後に同じ表を読むタイミングはCOMMIT後になります。ステップごとに処理ログを残しておくと、再実行もしやすくなります。

対策2:同じトランザクションで読む必要があるならAPPENDを外す

処理の途中で同じ表を確認し、その結果によってROLLBACKする必要があるなら、APPENDや並列DMLを使わない方が設計が単純です。高速化よりもトランザクションの一貫性が重要なケースです。

conventional-insert.sql
-- APPENDを使わない通常INSERT
INSERT INTO sales_work
SELECT *
FROM sales_source
WHERE sales_date >= DATE '2026-01-01';

-- 同じトランザクション内で確認できる
SELECT COUNT(*)
FROM sales_work;

-- 条件に応じてCOMMIT/ROLLBACKを選べる
COMMIT;

APPENDヒントやPARALLELヒントは速くなる可能性がありますが、制約も増えます。ヒント句全般は Oracle SQLヒント句完全ガイド で整理しています。

対策3:ステージング表を分ける

大量ロードと検証を両立したい場合は、ステージング表を明確に分けるのが実務的です。高速ロード用の表、検証用の表、最終反映先の表を分けることで、同じ表を同じトランザクションで触る場面を減らせます。

ロード表APPENDやNOLOGGINGを使って一気に取り込みます。ロード後にCOMMITします。
検証処理COMMIT後に件数、重複、必須値、外部キー相当のチェックを行います。
反映処理MERGEやINSERT SELECTで本表へ反映します。エラー行は別表へ逃がします。
再実行設計バッチID、ロード日時、ステータス列を持たせ、途中失敗しても再開できるようにします。

エラー行を逃がす設計は DBMS_ERRLOGとLOG ERRORSの使い方、FORALLで失敗行を拾う設計は FORALL SAVE EXCEPTIONSで失敗行を個別ログ化する方法 が関連します。

対策4:並列DMLを必要なセッションだけで有効化する

並列DMLは ALTER SESSION ENABLE PARALLEL DML で有効化します。便利ですが、セッション全体に影響します。バッチ内の一部処理だけで使いたい場合、どこで有効化し、どこで無効化するかを明確にします。

parallel-dml-session.sql
ALTER SESSION ENABLE PARALLEL DML;

INSERT /*+ PARALLEL(sales_stage 4) APPEND */ INTO sales_stage
SELECT /*+ PARALLEL(src 4) */ *
FROM sales_source src;

COMMIT;

ALTER SESSION DISABLE PARALLEL DML;

並列度を上げすぎると、CPU、I/O、UNDO、TEMP、ロック待ちに影響します。実行中のSQL調査は 遅いSQLを特定する方法完全ガイド、Data Pumpのパラレルは Data Pumpのパラレル処理で高速化する方法 が関連します。

APPENDとNOLOGGINGの注意点

APPENDは高速ロードでよく使いますが、同時にロック、領域使用、REDO、バックアップ運用も意識します。NOLOGGINGを組み合わせる場合、障害時にバックアップから完全復旧できるかを確認します。

APPENDダイレクト・パスINSERTを狙うヒントです。高速ですが、同一トランザクション内の再アクセス制限に注意します。
NOLOGGINGREDO削減に効く場合がありますが、バックアップ・リカバリ要件とセットで判断します。
CTAS既存表へ入れるより、CREATE TABLE AS SELECTで作ってから切り替える方が単純な場合があります。
索引大量ロード後の索引メンテナンスや統計情報収集も計画に入れます。

PL/SQLバッチでの安全な構成

PL/SQLの中でAPPENDや並列DMLを使う場合、処理の途中で同じ表を読んでいないかを確認します。特に、件数チェック、重複チェック、ログ出力、再処理判定のSQLが対象表を参照しているとORA-12838につながります。

plsql-batch-pattern.sql
BEGIN
  INSERT /*+ APPEND */ INTO sales_stage
  SELECT *
  FROM sales_source
  WHERE batch_id = :batch_id;

  COMMIT;

  -- COMMIT後に検証する
  validate_sales_stage(p_batch_id => :batch_id);

  merge_sales(p_batch_id => :batch_id);

  COMMIT;
EXCEPTION
  WHEN OTHERS THEN
    ROLLBACK;
    RAISE;
END;
/

途中COMMITが入るため、例外時に全体を元に戻す設計ではありません。ステップ単位の再実行、処理済みフラグ、バッチID、ログを用意します。

バッチ修正例:件数確認をCOMMIT後へ移す

batch-fix-count-after-commit.sql
-- NG: APPEND直後に同じ表を件数確認する
INSERT /*+ APPEND */ INTO import_stage
SELECT *
FROM import_source
WHERE batch_id = :batch_id;

-- SELECT COUNT(*) FROM import_stage; -- ここでORA-12838

-- OK: COMMIT後に確認する
COMMIT;

SELECT COUNT(*)
FROM import_stage
WHERE batch_id = :batch_id;

バッチ修正例:検証用の表を分ける

batch-fix-separate-stage.sql
-- 高速ロード専用の表
INSERT /*+ APPEND */ INTO import_stage_raw
SELECT *
FROM import_source
WHERE batch_id = :batch_id;

COMMIT;

-- 検証済みデータを別表へ移す
INSERT INTO import_stage_valid
SELECT *
FROM import_stage_raw
WHERE batch_id = :batch_id
  AND required_col IS NOT NULL;

COMMIT;

切り分けSQL

ORA-12838が出たSQLを見つけるには、エラーを出したバッチログ、アプリログ、SQL_ID、セッション情報を突き合わせます。並列DMLやAPPENDを使っている箇所を探します。

find-parallel-sql.sql
SELECT sql_id,
       parsing_schema_name,
       executions,
       sql_text
FROM v$sql
WHERE UPPER(sql_text) LIKE '%APPEND%'
   OR UPPER(sql_text) LIKE '%PARALLEL%'
   OR UPPER(sql_text) LIKE '%ENABLE PARALLEL DML%'
ORDER BY last_active_time DESC
FETCH FIRST 50 ROWS ONLY;
current-session-sql.sql
SELECT s.sid,
       s.serial#,
       s.username,
       s.module,
       s.action,
       s.sql_id,
       q.sql_text
FROM v$session s
LEFT JOIN v$sql q
  ON q.sql_id = s.sql_id
WHERE s.username IS NOT NULL
ORDER BY s.logon_time DESC;

アプリ側からMODULE/ACTIONを設定しておくと、どのバッチステップで起きたか追いやすくなります。

避けたい対応

原因を見ずにCOMMITを足す処理途中でCOMMITすると、後続失敗時に一括ROLLBACKできません。再実行設計が必要です。
APPENDを全INSERTに付ける小さなINSERTや途中検証が必要な処理では、制約の方が大きくなります。
並列DMLを常時有効にするセッション内の他のDMLまで影響し、予期しないORA-12838や負荷増加につながります。
同じ表でロード・検証・反映を完結させる高速化したい処理ほど、ステージング表やバッチIDで責務を分けます。

本番投入前チェックリスト

  • APPENDまたは並列DMLを使うSQLを一覧化している
  • その直後に同じ表をSELECT、UPDATE、DELETE、MERGEしていない
  • COMMIT境界が業務整合性と再実行設計に合っている
  • 同じトランザクションで確認が必要な処理ではAPPENDを外している
  • ステージング表、本表、エラー表の役割を分けている
  • 失敗時にどのステップから再実行するか決めている
  • NOLOGGINGを使う場合、バックアップ・リカバリ方針を確認している
  • 並列度がCPU、I/O、TEMP、UNDOに与える影響を測定している
  • SQL_ID、MODULE、ACTIONで原因SQLを追えるようにしている
  • 本番反映後、エラー件数とバッチ実行時間を監視する

まとめ

ORA-12838は、並列DMLやAPPENDで変更した表を、同じトランザクション内で再度読んだり変更したりした時に発生します。最短の対処はCOMMITまたはROLLBACKですが、処理途中にCOMMITを入れるなら、再実行設計と整合性設計も必要です。

高速ロードにはAPPENDや並列DMLが有効ですが、ロード、検証、反映を同じ表・同じトランザクションに詰め込むとORA-12838につながります。ステージング表、COMMIT境界、バッチID、エラー表を使い、速さと運用しやすさを両立させましょう。