【完全ガイド】ORA-04091: table is mutating の原因と解決方法|Compound Trigger・Autonomous Transaction まで徹底解説
- 作成日 2026.07.19
- Oracle Database
Oracle のトリガー開発で必ず遭遇する厄介なエラー:
CREATE OR REPLACE TRIGGER emp_biu
AFTER UPDATE ON employees
FOR EACH ROW
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count FROM employees WHERE status = 'ACTIVE';
END;
/
UPDATE employees SET status = 'ACTIVE' WHERE id = 100;
ORA-04091: table SCOTT.EMPLOYEES is mutating, trigger/function may not see it
ORA-06512: at "SCOTT.EMP_BIU", line 4
ORA-04088: error during execution of trigger 'SCOTT.EMP_BIU'
**「テーブルが変化中で、トリガー/関数から参照できない」という、一見すると意味不明なエラー。しかし、この裏にはOracle の Read Consistency(読み取り一貫性)**という重要な設計思想が隠れています。
現場では:
- トリガーで集計・検証したいのに動かない
- 監査ログを取ろうとしたら発生
- ネットの情報が古すぎる(10g 時代の Package 変数の方法)
PRAGMA AUTONOMOUS_TRANSACTIONで解決したはずが、実際は不整合が発生- Compound Trigger って何?
- Rails のマイグレーションで発生
- 本番リリース直前に発覚
このエラーの本質は、単純な設定ミスではなく、Oracle のトランザクション制御の設計上の制約にあります。日本語で本質を解説した記事はほぼなく、多くが「Autonomous Transaction を使え」という間違った解決策を提示。実は Autonomous Transaction はデータ不整合の温床で、正しくは Compound Trigger(Oracle 11g+) を使うべきです。
さらに、Compound Trigger の4つのタイミングポイントの使い分け、Package 変数を使った伝統的手法、Statement-level Trigger との比較など、正しく理解すればエレガントに解決できます。
本記事では、ORA-04091: table is mutating の完全な原因と解決方法を、日本語圏で類のない深さで整理します。Mutating table の仕組み、Read Consistency との関連、4大解決策(Statement-level・Compound・Autonomous・Package)、Compound Trigger の4タイミングポイント詳解、各手法の落とし穴、実践シナリオ、予防のベストプラクティス、FAQまで完全網羅。この1本でトリガー設計を根本から改善できるようになります。
- 1. 結論:4つの解決策から選ぶ
- 2. まず理解する:Mutating Table とは
- 3. 【解決策①】Statement-level Trigger(単純用途で最強)
- 4. 【解決策②】Compound Trigger(Oracle 11g+、本命)
- 5. 【解決策③】Autonomous Transaction(⚠️ 避けるべき)
- 6. 【解決策④】Package Variable(伝統的、11g 未満)
- 7. 発生パターン別対処
- 8. 実践シナリオ
- 9. 予防のベストプラクティス
- 10. トラブルシューティング
- 11. よくある質問(FAQ)
- 11.1. Q1. Compound Trigger は 10g で使える?
- 11.2. Q2. Autonomous Transaction は絶対ダメ?
- 11.3. Q3. BEFORE と AFTER の Mutating 違い
- 11.4. Q4. INSTEAD OF Trigger では発生する?
- 11.5. Q5. MERGE 文でも発生
- 11.6. Q6. パフォーマンスへの影響
- 11.7. Q7. Rails ActiveRecord での対応
- 11.8. Q8. Materialized View での代替
- 11.9. Q9. Autonomous DB での違い
- 11.10. Q10. RAC 環境での挙動
- 11.11. Q11. Trigger のパフォーマンス測定
- 11.12. Q12. Compound Trigger の全タイミング必要?
- 12. 参考リンク
- 13. まとめ
結論:4つの解決策から選ぶ
時間がない方向けに、最速の対処を先に示します。
4大解決策 比較
| # | 解決策 | Oracle バージョン | 難易度 | 推奨度 |
|---|---|---|---|---|
| ① | Statement-level Trigger | 全 | ⭐ | 単純用途で最強 |
| ② | Compound Trigger | 11g+ | ⭐⭐ | 本命 |
| ③ | Autonomous Transaction | 全 | ⭐ | ⚠️ 危険、避ける |
| ④ | Package Variable | 全 | ⭐⭐⭐ | 11g 未満の伝統手法 |
現代の推奨(Oracle 11g 以降)
Compound Trigger を使う。以下がテンプレート:
CREATE OR REPLACE TRIGGER emp_compound
FOR UPDATE OF status ON employees
COMPOUND TRIGGER
-- グローバル変数(statement 全体で共有)
v_count NUMBER;
BEFORE STATEMENT IS
BEGIN
v_count := 0;
END BEFORE STATEMENT;
AFTER EACH ROW IS
BEGIN
-- 各行の情報を集める
NULL;
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
-- ここでは同一テーブル参照OK!
SELECT COUNT(*) INTO v_count FROM employees WHERE status = 'ACTIVE';
DBMS_OUTPUT.PUT_LINE('Active count: ' || v_count);
END AFTER STATEMENT;
END emp_compound;
/
なぜ Autonomous Transaction を避けるか
-- ⚠️ 動くが不整合の温床
CREATE OR REPLACE TRIGGER bad_trigger
AFTER UPDATE ON employees FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
SELECT ... FROM employees ...; -- 動くが古いデータを見る
COMMIT;
END;
/
独立トランザクションで動くが、本トランザクションの変更が見えないため、古いデータを参照。結果、集計値が間違う。
詳細は以下で解説します。
まず理解する:Mutating Table とは
エラーメッセージの意味
ORA-04091: table SCOTT.EMPLOYEES is mutating,
trigger/function may not see it
翻訳:
- table is mutating: 「テーブルが変化中(mutate = 変異する)」
- may not see it: 「トリガー/関数から見ることはできない」
なぜ Oracle は禁止するか
Read Consistency(読み取り一貫性) の保証のため。
DML が実行中は、テーブルの状態が一貫していない(一部の行だけ更新済み)。この状態で SELECT すると、不整合なデータを見てしまう。
時刻 T1: UPDATE 開始
時刻 T2: 100行のうち 50行だけ更新済み
時刻 T3: トリガーが同じテーブルを SELECT
→ 半端な状態を見てしまう
→ データの整合性が保証できない
Oracle はこの状態を検出し、ORA-04091 で防止。
「Mutating」の適用範囲
Row-level Trigger のみが対象:
FOR EACH ROWあり → Mutating 制約適用FOR EACH ROWなし(Statement-level) → 制約なし
つまり、Statement-level Trigger なら同じテーブルを参照可能。
適用される DML
- INSERT
- UPDATE
- DELETE
- MERGE
すべての DML で発生の可能性。
BEFORE vs AFTER
BEFORE トリガーでも AFTER トリガーでも発生。タイミングに関係なく、Row-level では同一テーブル参照禁止。
【解決策①】Statement-level Trigger(単純用途で最強)
発想
FOR EACH ROW を外せば mutating 制約は適用されない:
-- ❌ Row-level(mutating エラー)
CREATE TRIGGER emp_biu
AFTER UPDATE ON employees
FOR EACH ROW -- ← これがある
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count FROM employees; -- ERROR
END;
-- ✅ Statement-level(動く)
CREATE TRIGGER emp_biu
AFTER UPDATE ON employees -- FOR EACH ROW なし
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count FROM employees; -- OK
END;
/
動作の違い
| 種類 | 発火頻度 | :NEW / :OLD |
|---|---|---|
| Row-level | 各行ごと | 使える |
| Statement-level | 文全体で1回 | 使えない |
使い分け
Statement-level が使えるケース:
- 各行の情報が必要ない
- 集計処理のみ
- 監査ログ(変更行数の記録など)
Row-level が必要なケース:
- 各行の
:NEW/:OLDを使う - 行ごとに何か処理
実践例:更新後の集計
CREATE OR REPLACE TRIGGER emp_after_update
AFTER UPDATE OF status ON employees
DECLARE
v_active_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_active_count
FROM employees WHERE status = 'ACTIVE';
DBMS_OUTPUT.PUT_LINE('Active employees: ' || v_active_count);
END;
/
UPDATE employees SET status = 'ACTIVE' WHERE id = 100;
-- 成功、Active count 表示
メリット・デメリット
メリット:
- シンプル
- 全 Oracle バージョンで動作
- 分かりやすい
デメリット:
:NEW/:OLD使えない- 各行の情報を集められない
【解決策②】Compound Trigger(Oracle 11g+、本命)
概要
Oracle 11g(2007年)で導入された機能。Row-level と Statement-level を1つのトリガーに統合、しかも共有変数を持てる。
4つのタイミングポイント
CREATE OR REPLACE TRIGGER trigger_name
FOR <event> ON <table>
COMPOUND TRIGGER
-- 共有変数(statement 全体で保持)
<declare variables>
BEFORE STATEMENT IS
BEGIN
-- statement 開始前に1回
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
-- 各行の直前
END BEFORE EACH ROW;
AFTER EACH ROW IS
BEGIN
-- 各行の直後
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
-- statement 完了後に1回
-- ここでは同一テーブル参照OK!
END AFTER STATEMENT;
END trigger_name;
/
実行順序
BEFORE STATEMENT (1回)
↓
[各行の処理]
BEFORE EACH ROW → DML → AFTER EACH ROW
BEFORE EACH ROW → DML → AFTER EACH ROW
BEFORE EACH ROW → DML → AFTER EACH ROW
↓
AFTER STATEMENT (1回) ← ここでは mutating 制約なし
実践例:行情報を集めて集計
CREATE OR REPLACE TRIGGER emp_salary_compound
FOR UPDATE OF salary ON employees
COMPOUND TRIGGER
-- 共有変数(コレクション)
TYPE t_ids IS TABLE OF NUMBER;
v_changed_ids t_ids := t_ids();
v_total_diff NUMBER := 0;
BEFORE STATEMENT IS
BEGIN
-- 初期化
v_changed_ids.DELETE;
v_total_diff := 0;
END BEFORE STATEMENT;
AFTER EACH ROW IS
BEGIN
-- 各行の情報を集める
v_changed_ids.EXTEND;
v_changed_ids(v_changed_ids.COUNT) := :NEW.id;
v_total_diff := v_total_diff + (:NEW.salary - :OLD.salary);
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
-- ここで同一テーブル参照OK!
DBMS_OUTPUT.PUT_LINE('Changed ' || v_changed_ids.COUNT || ' rows');
DBMS_OUTPUT.PUT_LINE('Total salary diff: ' || v_total_diff);
-- 監査ログ書き込み等
INSERT INTO audit_log(action, rows_affected, total_change)
VALUES ('SALARY_UPDATE', v_changed_ids.COUNT, v_total_diff);
END AFTER STATEMENT;
END emp_salary_compound;
/
全タイミング使用の完全例
CREATE OR REPLACE TRIGGER emp_full_compound
FOR INSERT OR UPDATE OR DELETE ON employees
COMPOUND TRIGGER
v_op VARCHAR2(10);
v_start_time TIMESTAMP;
v_row_count NUMBER := 0;
BEFORE STATEMENT IS
BEGIN
-- statement の種類を判定
IF INSERTING THEN v_op := 'INSERT';
ELSIF UPDATING THEN v_op := 'UPDATE';
ELSIF DELETING THEN v_op := 'DELETE';
END IF;
v_start_time := SYSTIMESTAMP;
DBMS_OUTPUT.PUT_LINE('=== ' || v_op || ' start at ' || v_start_time || ' ===');
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
-- 各行の直前検証
IF UPDATING AND :NEW.salary < :OLD.salary * 0.5 THEN
RAISE_APPLICATION_ERROR(-20001, '給与を50%以上下げられません');
END IF;
END BEFORE EACH ROW;
AFTER EACH ROW IS
BEGIN
v_row_count := v_row_count + 1;
END AFTER EACH ROW;
AFTER STATEMENT IS
v_total_active NUMBER;
BEGIN
-- ここで同一テーブル参照OK
SELECT COUNT(*) INTO v_total_active
FROM employees WHERE status = 'ACTIVE';
DBMS_OUTPUT.PUT_LINE(v_op || ' finished: ' || v_row_count || ' rows');
DBMS_OUTPUT.PUT_LINE('Total active: ' || v_total_active);
END AFTER STATEMENT;
END emp_full_compound;
/
メリット
- 単一トリガーで完結(複雑さを1箇所に集約)
- 共有変数が便利
- Mutating 制約を回避
- AFTER STATEMENT で同一テーブル参照OK
デメリット
- Oracle 11g+ 必要
- 学習コストがある
【解決策③】Autonomous Transaction(⚠️ 避けるべき)
動作
PRAGMA AUTONOMOUS_TRANSACTION で独立トランザクションを作成:
CREATE OR REPLACE TRIGGER bad_trigger
AFTER UPDATE ON employees
FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count FROM employees; -- 動く!
COMMIT; -- 必須
END;
/
エラーは出ず、動く。しかし…
問題点
メインの UPDATE 中の変更が見えない:
-- 現在: Active 100人
UPDATE employees SET status = 'ACTIVE' WHERE id = 200;
-- トリガー内で SELECT COUNT(*)
-- → 100 が返る(201 ではない!)
-- 理由: Autonomous Transaction は本トランザクション未完了の変更を見ない
具体的な不整合
-- 業務要件:Active 数を集計してログ
CREATE TRIGGER emp_audit
AFTER UPDATE OF status ON employees FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count FROM employees WHERE status = 'ACTIVE';
INSERT INTO audit_log(active_count, timestamp) VALUES (v_count, SYSDATE);
COMMIT;
END;
/
-- 初期: 100 active
UPDATE employees SET status = 'ACTIVE' WHERE id = 200;
COMMIT;
-- audit_log に active_count = 100 と記録される
-- 期待: 101
-- → 不整合発生!
なぜ多くの記事が推奨するか
- エラーが消える(見た目上動く)
- 一見シンプル
- 短時間の情報検索で「解決した」と誤解
でも実は業務ロジックが壊れている。
いつなら使ってもいい?
同一テーブルを参照しない用途なら OK:
- 別テーブルへのログ書き込み
- 外部システム呼び出し
- メール送信
同一テーブルを参照するなら絶対に避ける。
【解決策④】Package Variable(伝統的、11g 未満)
発想
セッション単位の Package 変数にデータを保存、後で使う。
手順
1. Package 定義:
CREATE OR REPLACE PACKAGE emp_pkg AS
TYPE t_ids IS TABLE OF NUMBER;
g_changed_ids t_ids := t_ids();
PROCEDURE add_id(p_id NUMBER);
PROCEDURE process_all;
END emp_pkg;
/
CREATE OR REPLACE PACKAGE BODY emp_pkg AS
PROCEDURE add_id(p_id NUMBER) IS
BEGIN
g_changed_ids.EXTEND;
g_changed_ids(g_changed_ids.COUNT) := p_id;
END;
PROCEDURE process_all IS
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count FROM employees WHERE status = 'ACTIVE';
DBMS_OUTPUT.PUT_LINE('Active: ' || v_count);
g_changed_ids.DELETE; -- クリア
END;
END emp_pkg;
/
2. Row-level Trigger でデータ収集:
CREATE OR REPLACE TRIGGER emp_row_trg
AFTER UPDATE ON employees FOR EACH ROW
BEGIN
emp_pkg.add_id(:NEW.id);
END;
/
3. Statement-level Trigger で処理:
CREATE OR REPLACE TRIGGER emp_stmt_trg
AFTER UPDATE ON employees
BEGIN
emp_pkg.process_all;
END;
/
メリット
- 11g 未満で使える
- 柔軟な処理
デメリット
- 複雑(Package + 2 Trigger)
- セッション変数管理の面倒
- Oracle 11g+ なら Compound Trigger でシンプルに
発生パターン別対処
パターン1: 集計値を計算したい
-- ❌ Mutating エラー
CREATE TRIGGER emp_row_trg
AFTER UPDATE ON employees FOR EACH ROW
DECLARE v_total NUMBER;
BEGIN
SELECT SUM(salary) INTO v_total FROM employees;
END;
/
対処: Compound Trigger の AFTER STATEMENT 部で。
パターン2: 制約検証(給与合計上限など)
-- 業務要件: 部署の給与合計は100万まで
-- ❌ Mutating
CREATE TRIGGER dept_check_trg
AFTER UPDATE OF salary ON employees FOR EACH ROW
DECLARE v_total NUMBER;
BEGIN
SELECT SUM(salary) INTO v_total
FROM employees WHERE dept_id = :NEW.dept_id;
IF v_total > 1000000 THEN
RAISE_APPLICATION_ERROR(-20001, '部署給与上限超過');
END IF;
END;
/
対処:
CREATE OR REPLACE TRIGGER dept_check_compound
FOR UPDATE OF salary ON employees
COMPOUND TRIGGER
TYPE t_depts IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
v_depts t_depts;
AFTER EACH ROW IS
BEGIN
v_depts(:NEW.dept_id) := 1; -- 影響部署を記録
END AFTER EACH ROW;
AFTER STATEMENT IS
v_dept_id NUMBER;
v_total NUMBER;
BEGIN
v_dept_id := v_depts.FIRST;
WHILE v_dept_id IS NOT NULL LOOP
SELECT SUM(salary) INTO v_total
FROM employees WHERE dept_id = v_dept_id;
IF v_total > 1000000 THEN
RAISE_APPLICATION_ERROR(-20001, '部署 ' || v_dept_id || ' 給与上限超過');
END IF;
v_dept_id := v_depts.NEXT(v_dept_id);
END LOOP;
END AFTER STATEMENT;
END dept_check_compound;
/
パターン3: 監査ログ
-- ❌ Mutating
CREATE TRIGGER audit_row
AFTER UPDATE ON employees FOR EACH ROW
BEGIN
INSERT INTO audit_log
SELECT SYSDATE, COUNT(*) FROM employees; -- Mutating
END;
/
対処: audit_log テーブルは別、employees の SELECT が問題:
- Compound Trigger で AFTER STATEMENT で処理
- または、行数を Row-level で数えて Statement-level で書き込む
パターン4: Function 経由の間接参照
-- ❌ 間接的に mutating
CREATE FUNCTION get_active_count RETURN NUMBER IS
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count FROM employees;
RETURN v_count;
END;
/
CREATE TRIGGER emp_trg
AFTER UPDATE ON employees FOR EACH ROW
DECLARE v_count NUMBER;
BEGIN
v_count := get_active_count; -- 内部で employees SELECT → Mutating
END;
/
対処: Compound Trigger の AFTER STATEMENT で関数呼び出し。
パターン5: Cascade Trigger
-- テーブル A の trigger が B を更新
-- B の trigger が A を SELECT → Mutating
対処: トリガー設計を見直し、Compound Trigger で1箇所に集約。
実践シナリオ
シナリオ1:シンプルな集計
-- 更新後、部門のアクティブ人数をログ
CREATE OR REPLACE TRIGGER dept_active_log
FOR UPDATE OF status ON employees
COMPOUND TRIGGER
TYPE t_depts IS TABLE OF PLS_INTEGER INDEX BY PLS_INTEGER;
v_depts t_depts;
AFTER EACH ROW IS
BEGIN
v_depts(:NEW.dept_id) := 1;
END AFTER EACH ROW;
AFTER STATEMENT IS
v_dept_id NUMBER;
v_count NUMBER;
BEGIN
v_dept_id := v_depts.FIRST;
WHILE v_dept_id IS NOT NULL LOOP
SELECT COUNT(*) INTO v_count
FROM employees
WHERE dept_id = v_dept_id AND status = 'ACTIVE';
INSERT INTO dept_status_log(dept_id, active_count, log_time)
VALUES (v_dept_id, v_count, SYSTIMESTAMP);
v_dept_id := v_depts.NEXT(v_dept_id);
END LOOP;
END AFTER STATEMENT;
END dept_active_log;
/
シナリオ2:業務ルール検証
-- 業務: 給与を50%以上下げてはいけない、
-- 部署予算超過は不可
CREATE OR REPLACE TRIGGER emp_business_rules
FOR UPDATE OF salary ON employees
COMPOUND TRIGGER
TYPE t_depts IS TABLE OF PLS_INTEGER INDEX BY PLS_INTEGER;
v_affected_depts t_depts;
BEFORE EACH ROW IS
BEGIN
-- 個別行チェック(同一テーブル参照なし)
IF :NEW.salary < :OLD.salary * 0.5 THEN
RAISE_APPLICATION_ERROR(-20001,
'給与を50%以上下げられません: ID ' || :NEW.id);
END IF;
END BEFORE EACH ROW;
AFTER EACH ROW IS
BEGIN
v_affected_depts(:NEW.dept_id) := 1;
END AFTER EACH ROW;
AFTER STATEMENT IS
v_dept_id NUMBER;
v_total NUMBER;
v_budget NUMBER;
BEGIN
v_dept_id := v_affected_depts.FIRST;
WHILE v_dept_id IS NOT NULL LOOP
SELECT SUM(salary) INTO v_total
FROM employees WHERE dept_id = v_dept_id;
SELECT budget INTO v_budget
FROM departments WHERE id = v_dept_id;
IF v_total > v_budget THEN
RAISE_APPLICATION_ERROR(-20002,
'部署 ' || v_dept_id || ' 予算超過: ' || v_total || ' > ' || v_budget);
END IF;
v_dept_id := v_affected_depts.NEXT(v_dept_id);
END LOOP;
END AFTER STATEMENT;
END emp_business_rules;
/
シナリオ3:親子テーブル同期
-- orders テーブル更新で、customers テーブルの last_order_date を更新
CREATE OR REPLACE TRIGGER order_sync_customer
FOR INSERT OR UPDATE ON orders
COMPOUND TRIGGER
TYPE t_cust_dates IS TABLE OF DATE INDEX BY PLS_INTEGER;
v_cust_dates t_cust_dates;
AFTER EACH ROW IS
BEGIN
IF NOT v_cust_dates.EXISTS(:NEW.customer_id) OR
v_cust_dates(:NEW.customer_id) < :NEW.order_date THEN
v_cust_dates(:NEW.customer_id) := :NEW.order_date;
END IF;
END AFTER EACH ROW;
AFTER STATEMENT IS
v_cust_id NUMBER;
BEGIN
v_cust_id := v_cust_dates.FIRST;
WHILE v_cust_id IS NOT NULL LOOP
UPDATE customers
SET last_order_date = v_cust_dates(v_cust_id)
WHERE id = v_cust_id;
v_cust_id := v_cust_dates.NEXT(v_cust_id);
END LOOP;
END AFTER STATEMENT;
END order_sync_customer;
/
シナリオ4:Rails マイグレーションでのトリガー作成
class CreateOrderSyncTrigger < ActiveRecord::Migration[8.0]
def up
execute <<-SQL
CREATE OR REPLACE TRIGGER order_sync_customer
FOR INSERT OR UPDATE ON orders
COMPOUND TRIGGER
-- ... 上記のロジック
END;
SQL
end
def down
execute "DROP TRIGGER order_sync_customer"
end
end
Rails 8 系の詳細は Rails 8 アップグレードガイドの記事、rails db:migrate 使い方の記事、Solid Queue 使い方の記事、find/find_by/where の記事も参照してください。
シナリオ5:Docker Oracle 開発環境
# Docker で開発中
docker exec -it oracle-xe sqlplus scott/tiger
# トリガー作成後テスト
UPDATE employees SET status='ACTIVE' WHERE id=100;
Docker 関連は docker daemon 接続エラーの記事、Docker no space left on device の記事も参照してください。
シナリオ6:Kamal デプロイでのトリガー配置
# config/deploy.yml
# デプロイ時にマイグレーションでトリガー作成
Kamal デプロイの詳細は Kamal 2 デプロイの記事、Kamal 使用時のジョブ処理は Solid Queue 使い方の記事、キャッシュ設定は Solid Cache 使い方の記事も参照してください。
シナリオ7:階層データの整合性チェック
-- 組織の階層構造、循環参照を防ぐ
CREATE OR REPLACE TRIGGER org_hierarchy_check
FOR INSERT OR UPDATE OF parent_id ON organizations
COMPOUND TRIGGER
TYPE t_ids IS TABLE OF NUMBER;
v_changed_ids t_ids := t_ids();
AFTER EACH ROW IS
BEGIN
v_changed_ids.EXTEND;
v_changed_ids(v_changed_ids.COUNT) := :NEW.id;
END AFTER EACH ROW;
AFTER STATEMENT IS
v_count NUMBER;
BEGIN
-- 各変更行から親をたどって循環しないかチェック
FOR i IN 1 .. v_changed_ids.COUNT LOOP
SELECT COUNT(*)
INTO v_count
FROM organizations
START WITH id = v_changed_ids(i)
CONNECT BY NOCYCLE PRIOR parent_id = id;
-- 循環検出時の処理
END LOOP;
END AFTER STATEMENT;
END org_hierarchy_check;
/
シナリオ8:在庫と注文の整合
CREATE OR REPLACE TRIGGER inventory_check
FOR UPDATE OF quantity ON order_items
COMPOUND TRIGGER
TYPE t_prods IS TABLE OF PLS_INTEGER INDEX BY PLS_INTEGER;
v_affected_prods t_prods;
AFTER EACH ROW IS
BEGIN
v_affected_prods(:NEW.product_id) := 1;
END AFTER EACH ROW;
AFTER STATEMENT IS
v_prod_id NUMBER;
v_ordered NUMBER;
v_stock NUMBER;
BEGIN
v_prod_id := v_affected_prods.FIRST;
WHILE v_prod_id IS NOT NULL LOOP
SELECT SUM(quantity) INTO v_ordered
FROM order_items WHERE product_id = v_prod_id;
SELECT stock INTO v_stock
FROM products WHERE id = v_prod_id;
IF v_ordered > v_stock THEN
RAISE_APPLICATION_ERROR(-20003,
'商品 ' || v_prod_id || ' 在庫不足: ' || v_ordered || ' > ' || v_stock);
END IF;
v_prod_id := v_affected_prods.NEXT(v_prod_id);
END LOOP;
END AFTER STATEMENT;
END inventory_check;
/
シナリオ9:本番リリース前のトリガー整合性チェック
-- 既存のトリガー一覧
SELECT trigger_name, triggering_event, table_name, status
FROM user_triggers
ORDER BY table_name, trigger_name;
-- Mutating の可能性があるトリガーを目視確認
SELECT trigger_name, trigger_body
FROM user_triggers
WHERE trigger_type LIKE '%EACH ROW%'
AND UPPER(trigger_body) LIKE '%SELECT%';
シナリオ10:Trigger を無効化してデータメンテナンス
-- メンテナンス中のみトリガー無効化
ALTER TRIGGER emp_business_rules DISABLE;
-- 大量データ投入
INSERT INTO employees SELECT * FROM staging_employees;
-- 再有効化
ALTER TRIGGER emp_business_rules ENABLE;
予防のベストプラクティス
1. まずトリガーの必要性を検討
制約で表現できるなら CHECK 制約や外部キー:
-- Trigger より
-- ✅ CHECK 制約
ALTER TABLE employees
ADD CONSTRAINT chk_salary CHECK (salary >= 0);
-- ✅ 外部キー
ALTER TABLE orders
ADD CONSTRAINT fk_customer
FOREIGN KEY (customer_id) REFERENCES customers(id);
制約 > トリガーが原則。
2. Row-level Trigger では同一テーブル参照禁止
-- ❌ 避ける
CREATE TRIGGER t
AFTER UPDATE ON emp FOR EACH ROW
BEGIN
SELECT ... FROM emp ...; -- Mutating の温床
END;
3. Compound Trigger を第一選択に
Oracle 11g+ なら:
CREATE OR REPLACE TRIGGER t
FOR UPDATE ON emp
COMPOUND TRIGGER
-- ...
END;
4. Autonomous Transaction は原則禁止
不整合の温床。同一テーブル参照する用途では絶対に使わない。
5. 監査は Oracle 純正 AUDIT 機能
トリガーで自作せず:
AUDIT UPDATE ON employees BY ACCESS;
メンテナンスコストが激減。
6. トリガーの単体テスト
-- テストデータで動作確認
BEGIN
UPDATE employees SET salary = 100000 WHERE id = 1;
ROLLBACK;
END;
/
7. トリガー数の制限
1テーブルに10個以上のトリガーは要注意。メンテナンス困難。
8. Cascade Trigger の避け方
トリガー A から B、B から A の循環はデッドロックのリスク。
デッドロック関連は、ORA-00060 デッドロックエラーの記事も参照してください。
トラブルシューティング
Autonomous Transaction で解決した「はず」
動くが不整合発生。集計値が期待と違う場合、Autonomous 使用を疑う:
SELECT trigger_name, trigger_body
FROM user_triggers
WHERE UPPER(trigger_body) LIKE '%AUTONOMOUS%';
該当があれば Compound Trigger に書き換え。
Compound Trigger でも Mutating
BEFORE EACH ROW / AFTER EACH ROW で同一テーブル参照するとエラー:
-- ❌ NG
AFTER EACH ROW IS
BEGIN
SELECT COUNT(*) INTO v_count FROM employees; -- Mutating!
END AFTER EACH ROW;
AFTER STATEMENT のみで同一テーブル参照OK。
RAISE_APPLICATION_ERROR が効かない
AFTER STATEMENT で発生したエラーは、トランザクション全体がロールバックされる。想定内の動作。
Trigger 内でループ
再帰トリガーで無限ループの可能性:
-- 対策: SESSION 変数でガード
CREATE PACKAGE session_pkg IS
g_in_trigger BOOLEAN := FALSE;
END;
/
CREATE TRIGGER t ...
BEGIN
IF NOT session_pkg.g_in_trigger THEN
session_pkg.g_in_trigger := TRUE;
-- ロジック
session_pkg.g_in_trigger := FALSE;
END IF;
END;
/
PL/SQL コンパイルエラー
SELECT text
FROM user_errors
WHERE name = 'MY_TRIGGER'
ORDER BY sequence;
エラー詳細を確認。
トリガー無効化
-- 個別
ALTER TRIGGER emp_business_rules DISABLE;
-- テーブルの全トリガー
ALTER TABLE employees DISABLE ALL TRIGGERS;
-- 再有効化
ALTER TABLE employees ENABLE ALL TRIGGERS;
よくある質問(FAQ)
Q1. Compound Trigger は 10g で使える?
11g(2007年) 以降のみ。10g では Package + 2 Trigger の伝統手法が必要。
Q2. Autonomous Transaction は絶対ダメ?
同一テーブル参照では絶対ダメ。別テーブルへのログ書き込みなら OK。
Q3. BEFORE と AFTER の Mutating 違い
関係なし。Row-level Trigger なら、BEFORE でも AFTER でも Mutating エラー発生。
Q4. INSTEAD OF Trigger では発生する?
ビューへの INSTEAD OF Triggerでは発生しない(実体テーブルを直接操作するため)。
Q5. MERGE 文でも発生
MERGE も DML なので発生の可能性:
MERGE INTO employees ...
-- Row-level Trigger 内で employees SELECT → Mutating
Q6. パフォーマンスへの影響
Compound Trigger は Row-level Trigger より遅い(各行で保存する処理あり)。ただし通常は許容範囲。
Q7. Rails ActiveRecord での対応
class Employee < ApplicationRecord
# Trigger より Rails のバリデーション/callback で処理
before_update :validate_business_rule
end
Rails 開発の詳細は Rails 8 アップグレードガイドの記事、find/find_by/where の記事、includes/preload/eager_load の記事も参照してください。
Q8. Materialized View での代替
集計トリガー代わりに MV:
CREATE MATERIALIZED VIEW mv_dept_summary
REFRESH FAST ON COMMIT
AS SELECT dept_id, COUNT(*) cnt FROM employees GROUP BY dept_id;
トリガーより高速でメンテナンス楽。
Q9. Autonomous DB での違い
Autonomous DB でも Compound Trigger 使用可。挙動は同じ。
Q10. RAC 環境での挙動
RAC でも Mutating 制約は同じ。インスタンス関係なし。
Q11. Trigger のパフォーマンス測定
SELECT trigger_name, trigger_type
FROM user_triggers WHERE table_name = 'EMPLOYEES';
-- 実行計画(トリガー内 SQL)
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(...));
Q12. Compound Trigger の全タイミング必要?
必要な部分だけ書けば OK:
CREATE TRIGGER t
FOR UPDATE ON emp
COMPOUND TRIGGER
AFTER STATEMENT IS
BEGIN
-- ここだけ
NULL;
END AFTER STATEMENT;
END;
/
参考リンク
Oracle 公式
- Oracle Database Error Messages: ORA-04091
- Compound DML Triggers
- PL/SQL Triggers
- Autonomous Transactions
まとめ
ORA-04091: table is mutating の解決、要点を再整理します。
エラーの本質
Row-level Trigger (FOR EACH ROW) 内で
同一テーブルを SELECT または DML
→ Read Consistency 保証のため Oracle が禁止
→ ORA-04091
4大解決策 比較
| # | 手法 | 推奨度 | 注意点 |
|---|---|---|---|
| ① | Statement-level Trigger | ⭐⭐⭐⭐ | :NEW/:OLD 使えない |
| ② | Compound Trigger | ⭐⭐⭐⭐⭐ | 本命、11g+ |
| ③ | Autonomous Transaction | ⚠️ | 不整合の温床 |
| ④ | Package Variable | ⭐⭐ | 11g 未満用 |
現代の推奨:Compound Trigger
CREATE OR REPLACE TRIGGER my_trigger
FOR UPDATE ON my_table
COMPOUND TRIGGER
-- 共有変数
v_count NUMBER;
BEFORE STATEMENT IS BEGIN v_count := 0; END BEFORE STATEMENT;
AFTER EACH ROW IS BEGIN NULL; END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
SELECT COUNT(*) INTO v_count FROM my_table WHERE ...;
-- 同一テーブル参照OK!
END AFTER STATEMENT;
END;
/
4つのタイミングポイント
- BEFORE STATEMENT: statement 開始前(1回)
- BEFORE EACH ROW: 各行の直前
- AFTER EACH ROW: 各行の直後
- AFTER STATEMENT: statement 完了後(1回)← 同一テーブル参照OK
Autonomous Transaction の罠
✅ エラーが消える
❌ 本トランザクションの変更が見えない
❌ 古いデータで集計 → 不整合
同一テーブル参照には絶対に使わない。
予防策
- 制約で表現できるならトリガー不要
- Row-level では同一テーブル参照しない
- Compound Trigger を第一選択
- Autonomous Transaction は避ける
- 監査は Oracle 純正 AUDIT 機能
- 単体テストで動作確認
- 1テーブルのトリガー数を制限
- Cascade Trigger を避ける
事故防止
- エラーが消えたら OK ではない(Autonomous の罠)
- 業務ロジックの一貫性を検証
- Compound Trigger で綺麗に書く
- トリガーはドキュメント化
モダンな代替
- Materialized View: 集計代替
- CHECK 制約: 単純ルール
- 外部キー: 参照整合性
- AUDIT: 監査
- アプリ側処理: Rails callback 等
これらの知識は、Oracle トリガー開発・ビジネスルール実装・監査ログ・データ整合性保証・Rails / Kamal 環境での DB 設計・レガシー Oracle 保守など、あらゆる場面で活用できます。本記事をブックマークしておけば、ORA-04091 に出会ってもエレガントに解決できるようになります。
本記事は2026年6月時点の情報をもとに、Oracle Database 19c〜23ai での動作確認・公式ドキュメントに基づき作成しています。Oracle のバージョンにより挙動が異なる場合があるため、最新の情報は Oracle 公式ドキュメント(docs.oracle.com)もあわせてご確認ください。
-
前の記事
【完全ガイド】ORA-04091: table is mutating の原因と解決方法|Compound Trigger・Autonomous Transaction まで徹底解説 2026.07.19
-
次の記事
【完全ガイド】Oracle パラメータ確認(V$PARAMETER)|V$系ビュー・ALTER SYSTEM・重要パラメータまで徹底解説 2026.07.20
コメントを書く