トリガーを使ったビジネスロジックの実装中に突然 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はこれをエラーとして防止しています。
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つに分ける方法が使われます。
-- ステップ 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 を外して文レベルトリガーにするのが最もシンプルです。
-- 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】トリガーを作成する方法」も参照してください。

