【Oracle】ORA-04091: 表は変化しています(ミューテーティングテーブル)の原因と解決方法完全ガイド

【Oracle】ORA-04091: 表は変化しています(ミューテーティングテーブル)の原因と解決方法完全ガイド Oracle

トリガーを使ったビジネスロジックの実装中に突然 ORA-04091: table XXXX is mutating, trigger/function may not see it が発生して戸惑った経験はないでしょうか。これはOracleのミューテーティングテーブル制約に起因するエラーで、行トリガーの中からトリガーが発火したテーブル自身をSELECTしようとすると発生します。本記事では発生メカニズムから、コンパウンドトリガーやパッケージ変数を使った解決パターンまで体系的に解説します。

この記事でわかること

  • ミューテーティングテーブルとは何か(ORA-04091 の発生メカニズム)
  • 行トリガーで自テーブルを SELECT すると発生する基本パターン
  • コンパウンドトリガー(Oracle 11g+)による解決方法
  • パッケージ変数を使った従来の回避策(Oracle 10g以前対応)
  • STATEMENT レベルトリガーへの切り替え
  • PRAGMA AUTONOMOUS_TRANSACTION を使うべきでない理由
スポンサーリンク

ミューテーティングテーブルとは

ミューテーティングテーブル(Mutating Table)とは、INSERT / UPDATE / DELETE の実行によって現在変更中のテーブルのことです。Oracleは、行レベルトリガー(FOR EACH ROW)の内部から、トリガーが発火した自テーブルに対するSELECTを禁止しています。

その理由は、行トリガーは1行ずつ発火するため、テーブルの内容が「変更途中の中間状態」にあるからです。この中間状態でSELECTすると、処理の順序によって結果が変わる非決定的な動作になるため、Oracleはこれをエラーとして防止しています。

ORA-04091 が発生する最もシンプルな例
CREATE TABLE employees (
    employee_id   NUMBER PRIMARY KEY,
    name          VARCHAR2(100),
    salary        NUMBER,
    department_id NUMBER
);

-- 行トリガー:給与が部門平均を超えていないかチェックする
CREATE OR REPLACE TRIGGER trg_check_salary
BEFORE INSERT OR UPDATE OF salary ON employees
FOR EACH ROW
DECLARE
    v_avg_salary NUMBER;
BEGIN
    -- ★ ここで自テーブル employees を SELECT している → ORA-04091
    SELECT AVG(salary) INTO v_avg_salary
    FROM employees
    WHERE department_id = :NEW.department_id;

    IF :NEW.salary > v_avg_salary * 3 THEN
        RAISE_APPLICATION_ERROR(-20001,
            '給与が部門平均の3倍を超えています');
    END IF;
END;
/

-- テスト
UPDATE employees SET salary = 9999999 WHERE employee_id = 1;
-- → ORA-04091: table EMPLOYEES is mutating, trigger/function may not see it

ORA-04091 が発生する条件と発生しない条件

条件 発生するか 理由
行トリガー(FOR EACH ROW)で自テーブルをSELECT 発生する テーブルが変更中(ミューテーティング)のため
行トリガーで別テーブルをSELECT 発生しない 別テーブルは変更中ではないため
行トリガーで :NEW / :OLD を参照 発生しない 行変数は直接参照可能
文トリガー(FOR EACH STATEMENT)で自テーブルをSELECT 発生しない 文トリガーはDML完了後に発火するため
1行だけのINSERTで自テーブルをSELECT 発生しない場合がある Oracle内部最適化で回避されるケースがあるが、保証されない

解決策:コンパウンドトリガー(Oracle 11g 以降・推奨)

コンパウンドトリガー(Compound Trigger)は、1つのトリガー定義の中で「行レベル処理」と「文レベル処理」を組み合わせることができるOracle 11g以降の機能です。行レベルでデータを収集し、文レベルで集計処理を実行することで、ミューテーティングテーブル問題を根本的に解決できます。

コンパウンドトリガー ── 推奨される解決パターン
CREATE OR REPLACE TRIGGER trg_check_salary
FOR INSERT OR UPDATE OF salary ON employees
COMPOUND TRIGGER

    -- ★ トリガー内で共有する変数(パッケージ変数の代わり)
    TYPE t_dept_ids IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
    g_dept_ids t_dept_ids;
    g_idx      PLS_INTEGER := 0;

    -- ========== 行レベル: 変更された部門IDを記録する ==========
    AFTER EACH ROW IS
    BEGIN
        g_idx := g_idx + 1;
        g_dept_ids(g_idx) := :NEW.department_id;
    END AFTER EACH ROW;

    -- ========== 文レベル: DML完了後にまとめてチェックする ==========
    AFTER STATEMENT IS
        v_avg_salary NUMBER;
        v_max_salary NUMBER;
    BEGIN
        FOR i IN 1..g_idx LOOP
            -- ★ ここでは DML が完了しているので自テーブルを安全に SELECT できる
            SELECT AVG(salary), MAX(salary)
            INTO v_avg_salary, v_max_salary
            FROM employees
            WHERE department_id = g_dept_ids(i);

            IF v_max_salary > v_avg_salary * 3 THEN
                RAISE_APPLICATION_ERROR(-20001,
                    '部門' || g_dept_ids(i) || 'に給与が平均の3倍を超える社員がいます');
            END IF;
        END LOOP;
    END AFTER STATEMENT;

END trg_check_salary;
/
コンパウンドトリガーの仕組み
コンパウンドトリガーは最大4つのタイミングセクションを持てます。

  • BEFORE STATEMENT:文の実行前に1回
  • BEFORE EACH ROW:各行の処理前に行ごとに
  • AFTER EACH ROW:各行の処理後に行ごとに
  • AFTER STATEMENT:文の実行後に1回

行レベルでデータを収集し、文レベルで自テーブルを参照する――このパターンがミューテーティングテーブル問題の標準的な解決策です。

解決策:パッケージ変数を使った回避策(Oracle 10g以前対応)

コンパウンドトリガーが使えない環境(Oracle 10g以前)では、パッケージ変数にデータを一時保存し、行トリガーと文トリガーを2つに分ける方法が使われます。

パッケージ変数 + 2つのトリガーによる回避
-- ステップ 1: データ一時保存用のパッケージを作成
CREATE OR REPLACE PACKAGE pkg_emp_check AS
    TYPE t_dept_ids IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
    g_dept_ids t_dept_ids;
    g_idx      PLS_INTEGER := 0;
    PROCEDURE reset;
END pkg_emp_check;
/

CREATE OR REPLACE PACKAGE BODY pkg_emp_check AS
    PROCEDURE reset IS
    BEGIN
        g_dept_ids.DELETE;
        g_idx := 0;
    END reset;
END pkg_emp_check;
/

-- ステップ 2: 行トリガーで変更された部門IDを記録
CREATE OR REPLACE TRIGGER trg_emp_row
AFTER INSERT OR UPDATE OF salary ON employees
FOR EACH ROW
BEGIN
    pkg_emp_check.g_idx := pkg_emp_check.g_idx + 1;
    pkg_emp_check.g_dept_ids(pkg_emp_check.g_idx) := :NEW.department_id;
END;
/

-- ステップ 3: 文トリガーでまとめてチェック
CREATE OR REPLACE TRIGGER trg_emp_stmt
AFTER INSERT OR UPDATE OF salary ON employees
DECLARE
    v_avg_salary NUMBER;
    v_max_salary NUMBER;
BEGIN
    FOR i IN 1..pkg_emp_check.g_idx LOOP
        SELECT AVG(salary), MAX(salary)
        INTO v_avg_salary, v_max_salary
        FROM employees
        WHERE department_id = pkg_emp_check.g_dept_ids(i);

        IF v_max_salary > v_avg_salary * 3 THEN
            RAISE_APPLICATION_ERROR(-20001,
                '部門' || pkg_emp_check.g_dept_ids(i) || 'に給与が平均の3倍を超える社員がいます');
        END IF;
    END LOOP;
    pkg_emp_check.reset;  -- 次回の DML に備えてリセット
END;
/

解決策:文レベルトリガーへの切り替え

そもそも :NEW / :OLD の値を使わなくてよい場合は、FOR EACH ROW を外して文レベルトリガーにするのが最もシンプルです。

文レベルトリガー ── FOR EACH ROW を外す
-- DML完了後にテーブル全体をチェックする文レベルトリガー
CREATE OR REPLACE TRIGGER trg_check_salary_stmt
AFTER INSERT OR UPDATE OF salary ON employees
-- ★ FOR EACH ROW を書かない → 文レベルトリガーになる
DECLARE
    v_violation_count NUMBER;
BEGIN
    SELECT COUNT(*) INTO v_violation_count
    FROM (
        SELECT department_id, salary, AVG(salary) OVER (PARTITION BY department_id) AS avg_sal
        FROM employees
    )
    WHERE salary > avg_sal * 3;

    IF v_violation_count > 0 THEN
        RAISE_APPLICATION_ERROR(-20001,
            '給与が部門平均の3倍を超える社員が' || v_violation_count || '名います');
    END IF;
END;
/

PRAGMA AUTONOMOUS_TRANSACTION は使うべきでない

ミューテーティングテーブルエラーの「回避策」として PRAGMA AUTONOMOUS_TRANSACTION を使うコード例がインターネット上にありますが、この方法は推奨されません

問題点 説明
一貫性がない 自律トランザクションは親トランザクションの未コミットデータを参照できない。変更前の古いデータでチェックが行われる
バグの温床 トリガー内のDMLが親とは別にCOMMIT/ROLLBACKされるため、データの整合性が保証されない
デバッグが困難 2つの独立したトランザクションが絡み合い、問題の再現・特定が極めて難しくなる

自律トランザクションは監査ログの書き込み等「親トランザクションの結果に依存しない処理」にのみ使うべきです。ミューテーティングテーブル問題にはコンパウンドトリガーまたはパッケージ変数方式を使ってください。

解決策の比較

解決策 対応バージョン 複雑さ 推奨度
コンパウンドトリガー 11g 以降 低い(1つのトリガーで完結) ★★★ 最も推奨
パッケージ変数 + 2トリガー 全バージョン 中程度(パッケージ + 2トリガー) ★★ 10g以前では唯一の選択肢
文レベルトリガーに変更 全バージョン 低い ★★★ :NEW/:OLD不要なら最適
AUTONOMOUS_TRANSACTION 全バージョン 低い ★ 非推奨(一貫性が保証されない)

まとめ

ORA-04091 は、行レベルトリガーの中から変更中の自テーブルをSELECTしたときに発生するOracleの制約エラーです。

  • 行トリガー(FOR EACH ROW)の中では、トリガーが発火したテーブル自身を SELECT/INSERT/UPDATE/DELETE できない
  • :NEW / :OLD は直接参照可能。別テーブルへのアクセスも問題ない
  • コンパウンドトリガー(11g+)が最も推奨される解決策。行レベルでデータを収集し、文レベルで自テーブルを参照する
  • 10g以前ではパッケージ変数 + 行トリガー + 文トリガーの3点セットで回避する
  • :NEW/:OLD が不要な場合は文レベルトリガーFOR EACH ROW を外す)が最もシンプル
  • PRAGMA AUTONOMOUS_TRANSACTION は一貫性が保証されないため、この問題の回避策としては使わない

トリガーの作成・管理については「【Oracle】トリガーを作成する方法」も参照してください。