【PL/SQL】トリガー完全ガイド|6種類の使い分け・Compound Trigger・Mutating Table回避・INSTEAD OF・実務5パターン

【PL/SQL】トリガーを作成する方法と具体例 PL/SQL

Oracleのトリガー(Trigger)は、テーブルのDML(INSERT/UPDATE/DELETE)やDDL、ログオン/ログオフ等のイベントに自動的に反応して処理を実行する仕組みです。監査ログ・更新日時自動記録・楽観的ロック・データ整合性維持など、実務で幅広く使われます。

しかし入門記事の多くはBEFORE INSERT ... FOR EACH ROWの基本形までで止まっていて、実務で本当に必要なMutating Table例外(ORA-04091)の回避、Compound Triggerによる4フェーズ制御(12c以降)、INSTEAD OFトリガーによるビュー更新、System Trigger(DDL/ログオン監視)、パフォーマンス影響の実測、トリガーのデバッグ手法までまとまった情報源はほとんどありません。

この記事ではOracleトリガーを基礎から運用レベルまで徹底解説します。トリガー6種類の全体像、:NEW/:OLD擬似レコード、WHEN句でのガード、Compound Triggerで4フェーズ制御+Mutating Table回避、INSTEAD OFトリガー、System Trigger、実務実装5パターン(監査/更新日時/楽観的ロック/整合性/カスケード)、パフォーマンス最適化、アンチパターン7選まで網羅した決定版です。関連は【PL/SQL】例外処理完全ガイド【PL/SQL】IF文完全ガイド【PL/SQL】変数・定数完全ガイドも併読推奨。

この記事で学べること

  • Oracleトリガーの6種類と選び方(DML/INSTEAD OF/System/Compound)
  • DMLトリガー:BEFORE/AFTER×Row/Statementの4象限
  • :NEW:OLD擬似レコードのINSERT/UPDATE/DELETE別挙動
  • WHEN (条件)句で発火条件を絞る
  • Compound Trigger(12c以降)の4フェーズ制御
  • Mutating Table問題(ORA-04091)の原因と3つの回避策
  • INSTEAD OFトリガーでビューを更新可能にする
  • System Trigger(DDL監視・ログオン・スキーマ監査)
  • 実務5パターン:監査ログ/updated_at自動/楽観的ロック/正規化維持/カスケード
  • ALTER TRIGGER DISABLEENABLEDROPの管理
  • パフォーマンス最適化とトリガー無効化時のメンテナンス
  • アンチパターン7選(ビジネスロジック肥大化/連鎖発火/COMMIT禁止等)
スポンサーリンク

30秒クイックリファレンス:トリガー基本構文

トリガー基本テンプレ
CREATE OR REPLACE TRIGGER <トリガー名>
{BEFORE | AFTER | INSTEAD OF}
  {INSERT | UPDATE [OF 列名] | DELETE}
  [OR {INSERT | UPDATE | DELETE}] ...
ON <テーブル名|ビュー名>
[FOR EACH ROW]              -- 行トリガー(省略時はstatement)
[WHEN (<発火条件>)]       -- 条件式(NEW/OLDは : なし)
[DECLARE
  -- ローカル変数
]
BEGIN
  -- 実行したい処理
  -- :NEW.col で新値、:OLD.col で旧値にアクセス
[EXCEPTION
  -- 例外ハンドラ
]
END;
/
最小動作例:updated_at自動更新
CREATE OR REPLACE TRIGGER trg_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
  :NEW.updated_at := SYSTIMESTAMP;
END;
/

-- これだけで UPDATE users ... のたびに updated_at が自動でSYSTIMESTAMPに

実務で使うトリガーの80%は3パターン:BEFORE UPDATE FOR EACH ROW(:NEW書き換えで updated_at 等の自動設定)、②AFTER INSERT/UPDATE/DELETE FOR EACH ROW(監査ログ記録)、③INSTEAD OF INSERT/UPDATE/DELETE(ビューを更新可能にする)。他の種類は特殊用途。

トリガー6種類の全体像

Oracleトリガーは用途で大きく6種類に分類されます。用途で自動的に選択肢が絞られます。

種類 対象イベント 主な用途
Row Trigger(行) DML実行各行ごと updated_at自動/監査ログ/値検証
Statement Trigger(文) DML文全体で1回 件数集計・開始/終了ログ
INSTEAD OF Trigger ビューへのDML 複雑ビューを更新可能に
Compound Trigger(12c+) DML全体+各行 4フェーズ制御・Mutating Table回避
System Trigger (DDL) CREATE/ALTER/DROP等 スキーマ変更監査
System Trigger (Database) LOGON/LOGOFF/STARTUP/SHUTDOWN ログオン監査/起動時初期化

DMLトリガーの4象限(BEFORE/AFTER × Row/Statement)

4つの組み合わせ
                  Row (FOR EACH ROW)       Statement (省略)
               ┌──────────────────────┬──────────────────────┐
BEFORE         │ ①:NEW書き換え        │ ③事前ログ/集計       │
               │  (updated_at等)     │  (実行前状態の記録)  │
               ├──────────────────────┼──────────────────────┤
AFTER          │ ②監査ログ/通知       │ ④事後集計/通知       │
               │  (:NEW/:OLDが確定後)│  (件数/完了ログ)    │
               └──────────────────────┴──────────────────────┘

選び方のコツ

  • 値の書き換え(検証・補完)→BEFORE ROW:NEWに代入)
  • 監査ログ/通知→AFTER ROW(確定値を使える)
  • ビュー経由のDML→INSTEAD OF
  • 同じテーブルを集計したい→Compound Trigger(Mutating Table回避)
  • 1万件一括DMLで件数だけ記録→AFTER STATEMENT(性能影響最小)

:NEW/:OLD 擬似レコード完全解説

操作 :OLD :NEW
INSERT 全列NULL(挿入前の行は無い) 挿入される新値。BEFOREで書き換え可能
UPDATE 更新前の行 更新後の値。BEFOREで書き換え可能
DELETE 削除される行 全列NULL(削除後の行は無い)
:NEW/:OLDの使い方
CREATE OR REPLACE TRIGGER trg_orders
BEFORE INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW
BEGIN
  -- 操作種別判定(Event Predicates)
  IF INSERTING THEN
    :NEW.created_at := SYSTIMESTAMP;
    :NEW.updated_at := SYSTIMESTAMP;
    :NEW.version    := 1;

  ELSIF UPDATING THEN
    :NEW.updated_at := SYSTIMESTAMP;
    :NEW.version    := NVL(:OLD.version, 0) + 1;
    -- 新旧値を比較してログ
    IF NVL(:OLD.status, ' ') != NVL(:NEW.status, ' ') THEN
      INSERT INTO audit_log VALUES (
        :OLD.id, 'STATUS_CHANGE',
        :OLD.status || ' → ' || :NEW.status,
        SYSTIMESTAMP
      );
    END IF;

  ELSIF DELETING THEN
    -- 削除前のデータをアーカイブに保存
    INSERT INTO orders_archive VALUES (
      :OLD.id, :OLD.user_id, :OLD.amount, :OLD.created_at, SYSTIMESTAMP
    );
  END IF;
END;

重要な制約::NEWへの代入はBEFORE ROWトリガーでのみ可能(AFTERは不可)、②:NEW:OLDRow Triggerでのみ使える(Statementでは使えない)、③UPDATING('列名')で特定列の更新判定も可能(IF UPDATING('salary') THEN ...)、④WHEN句内ではコロン無し(NEW.colOLD.col)。

WHEN句で発火条件を絞る

WHEN句の使い方
-- 給与が変わった時だけ発火
CREATE OR REPLACE TRIGGER trg_salary_audit
AFTER UPDATE OF salary ON employees
FOR EACH ROW
WHEN (NEW.salary <> OLD.salary OR
      (NEW.salary IS NULL AND OLD.salary IS NOT NULL) OR
      (NEW.salary IS NOT NULL AND OLD.salary IS NULL))
BEGIN
  INSERT INTO salary_audit(emp_id, old_salary, new_salary, changed_at)
  VALUES (:NEW.id, :OLD.salary, :NEW.salary, SYSTIMESTAMP);
END;
/

-- 特定ステータスへの変更だけ通知
CREATE OR REPLACE TRIGGER trg_order_shipped
AFTER UPDATE OF status ON orders
FOR EACH ROW
WHEN (NEW.status = 'shipped' AND OLD.status != 'shipped')
BEGIN
  send_shipment_notification(:NEW.id);
END;

WHEN句の注意点

  • コロン無しNEW.colOLD.colと書く(トリガー本体とは記法が違う)
  • NULL比較は必ずIS NULLNEW.col <> OLD.colはNULLの時にFALSE扱いで漏れる)
  • BEGIN本体でIFするよりWHEN句で絞る方がオーバーヘッドが小さく高速
  • Row Triggerでのみ使用可(Statement Triggerには無い)

Compound Trigger(複合トリガー):12c以降の決定版

11g Release 1以降で追加されたCompound Triggerは、4つのフェーズ(BEFORE STATEMENT/BEFORE EACH ROW/AFTER EACH ROW/AFTER STATEMENT)を1つのトリガー内で定義でき、フェーズ間で状態を共有できます。Mutating Table問題の解決にも最も簡潔。

Compound Trigger基本構造
CREATE OR REPLACE TRIGGER trg_orders_compound
FOR INSERT OR UPDATE OR DELETE ON orders
COMPOUND TRIGGER

  -- ★状態変数:全フェーズで共有
  TYPE t_ids IS TABLE OF orders.id%TYPE INDEX BY PLS_INTEGER;
  v_changed_ids t_ids;
  v_change_count PLS_INTEGER := 0;

  BEFORE STATEMENT IS
  BEGIN
    -- DML開始前の初期化
    v_changed_ids.DELETE;
    v_change_count := 0;
    DBMS_OUTPUT.PUT_LINE('DML開始');
  END BEFORE STATEMENT;

  BEFORE EACH ROW IS
  BEGIN
    -- 各行のBEFORE処理
    IF INSERTING OR UPDATING THEN
      :NEW.updated_at := SYSTIMESTAMP;
    END IF;
  END BEFORE EACH ROW;

  AFTER EACH ROW IS
  BEGIN
    -- 各行のAFTER処理(IDを溜めるだけ)
    v_change_count := v_change_count + 1;
    IF INSERTING OR UPDATING THEN
      v_changed_ids(v_change_count) := :NEW.id;
    ELSE
      v_changed_ids(v_change_count) := :OLD.id;
    END IF;
  END AFTER EACH ROW;

  AFTER STATEMENT IS
  BEGIN
    -- DML完了後、蓄積したIDに対して処理(Mutating Tableを避ける鍵)
    FOR i IN 1..v_changed_ids.COUNT LOOP
      INSERT INTO audit_log(table_name, row_id, action, changed_at)
      VALUES ('orders', v_changed_ids(i), 'BULK_CHANGE', SYSTIMESTAMP);
    END LOOP;
    DBMS_OUTPUT.PUT_LINE(v_change_count || '件完了');
  END AFTER STATEMENT;

END trg_orders_compound;
/

Compound Triggerの3つのメリット:4フェーズを1トリガーで管理(関連する処理を近くに書ける)、②フェーズ間で状態共有(Row→Statementで値を持ち越せる)、③Mutating Table問題を綺麗に回避(次セクションで解説)。11g以降の新規トリガーは積極的に使うべき構文。

Mutating Table問題(ORA-04091)と3つの解決策

Mutating Table(変化中のテーブル)とは、Row Triggerの実行中に同じテーブルをSELECT/UPDATE/DELETEしようとすると発生するエラー。Oracleは「DMLで変化中のテーブルを参照すると一貫性が保てない」としてORA-04091で防いでいます。

❌ Mutating Table例外を起こすコード
CREATE OR REPLACE TRIGGER trg_check_max_salary
BEFORE INSERT OR UPDATE ON employees
FOR EACH ROW
DECLARE
  v_max_salary NUMBER;
BEGIN
  -- 同じemployeesテーブルをSELECT → ORA-04091
  SELECT MAX(salary) INTO v_max_salary FROM employees;

  IF :NEW.salary &gt; v_max_salary * 1.5 THEN
    RAISE_APPLICATION_ERROR(-20001, '最高給与の1.5倍を超えています');
  END IF;
END;
/

-- INSERT実行時のエラー
-- ORA-04091: table SCHEMA.EMPLOYEES is mutating, trigger/function may not see it

解決策①:Compound Triggerで分離

BEFORE STATEMENT でスナップショット取得
CREATE OR REPLACE TRIGGER trg_check_salary_compound
FOR INSERT OR UPDATE ON employees
COMPOUND TRIGGER

  v_max_salary NUMBER;

  BEFORE STATEMENT IS
  BEGIN
    -- DML開始前(テーブルはまだmutating前)に集計
    SELECT MAX(salary) INTO v_max_salary FROM employees;
  END BEFORE STATEMENT;

  BEFORE EACH ROW IS
  BEGIN
    -- 各行で比較(SELECTしない)
    IF :NEW.salary &gt; v_max_salary * 1.5 THEN
      RAISE_APPLICATION_ERROR(-20001, '最高給与の1.5倍を超えています');
    END IF;
  END BEFORE EACH ROW;

END;

解決策②:PRAGMA AUTONOMOUS_TRANSACTIONで独立トランザクション

ただし副作用に注意
CREATE OR REPLACE TRIGGER trg_log_with_autonomous
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW
DECLARE
  PRAGMA AUTONOMOUS_TRANSACTION;
  v_count NUMBER;
BEGIN
  -- 自律トランザクション内での別操作
  INSERT INTO audit_log(...) VALUES (...);
  COMMIT;
END;

AUTONOMOUS_TRANSACTIONの罠:独立したトランザクションのため元のトランザクションがROLLBACKしても自律側のCOMMITは戻らない。監査ログの永続化には便利ですが、誤って整合性が必要な処理に使うとデータ不整合を起こします。【PL/SQL】AUTONOMOUS TRANSACTIONで独立した処理を行う方法で詳細解説。

解決策③:Row Trigger→Statement Triggerへ分離

古い王道パターン(Compound前)
-- パッケージで状態を保持
CREATE OR REPLACE PACKAGE pkg_emp_check IS
  TYPE t_ids IS TABLE OF employees.id%TYPE INDEX BY PLS_INTEGER;
  g_ids t_ids;
  g_count PLS_INTEGER;
END;
/

-- Row Triggerで対象行のIDだけ溜める
CREATE OR REPLACE TRIGGER trg_emp_row
AFTER INSERT OR UPDATE ON employees
FOR EACH ROW
BEGIN
  pkg_emp_check.g_count := pkg_emp_check.g_count + 1;
  pkg_emp_check.g_ids(pkg_emp_check.g_count) := :NEW.id;
END;
/

-- Statement TriggerでまとめてSELECT可能
CREATE OR REPLACE TRIGGER trg_emp_stmt
AFTER INSERT OR UPDATE ON employees
BEGIN
  -- ここではemployeesのSELECTが可能
  FOR i IN 1..pkg_emp_check.g_count LOOP
    -- 集計処理...
    NULL;
  END LOOP;
  pkg_emp_check.g_ids.DELETE;
  pkg_emp_check.g_count := 0;
END;

現代の推奨:Compound Trigger一択。3つのアプローチがありますが、11g以降ならCompound Triggerが①可読性、②フェーズ分離、③状態保持の全てで最適。レガシーコードで解決策③を見たらCompound Triggerへのリファクタを検討しましょう。

INSTEAD OFトリガー:ビューを更新可能にする

複数テーブルをJOINしたビューはデフォルトでINSERT/UPDATE/DELETE不可ORA-01776: cannot modify more than one base table through a join view)ですが、INSTEAD OFトリガーを定義するとビュー経由のDMLを「代替処理」に置き換えられます。

INSTEAD OFトリガーの例
-- 複数テーブルを結合したビュー
CREATE OR REPLACE VIEW v_user_orders AS
SELECT u.id AS user_id, u.email,
       o.id AS order_id, o.amount, o.status
FROM users u JOIN orders o ON o.user_id = u.id;

-- このビューにINSERTするトリガー
CREATE OR REPLACE TRIGGER trg_user_orders_ins
INSTEAD OF INSERT ON v_user_orders
FOR EACH ROW
BEGIN
  -- users側をUPSERT
  BEGIN
    INSERT INTO users(id, email) VALUES (:NEW.user_id, :NEW.email);
  EXCEPTION
    WHEN DUP_VAL_ON_INDEX THEN
      UPDATE users SET email = :NEW.email WHERE id = :NEW.user_id;
  END;

  -- orders側をINSERT
  INSERT INTO orders(id, user_id, amount, status)
  VALUES (:NEW.order_id, :NEW.user_id, :NEW.amount, :NEW.status);
END;
/

-- 使用側:ビュー経由で複数テーブル更新
INSERT INTO v_user_orders(user_id, email, order_id, amount, status)
VALUES (1, 'test@example.com', 100, 5000, 'pending');

INSTEAD OFの典型用途

  • 複雑なJOIN ビューをORM/BIツールから更新可能に
  • 論理削除(is_deleted=1)の実装をアプリから隠蔽
  • 古いテーブル互換のための「互換ビュー」
  • 暗号化カラムの透過的読み書き

System Trigger:DDL・ログオン監査

DDLトリガーの例(スキーマ変更監査)
CREATE OR REPLACE TRIGGER trg_ddl_audit
AFTER DDL ON SCHEMA    -- またはDATABASE
BEGIN
  INSERT INTO ddl_audit_log(
    event_time, username, event_type,
    object_type, object_name, sql_text
  ) VALUES (
    SYSTIMESTAMP,
    SYS_CONTEXT('USERENV', 'SESSION_USER'),
    ora_sysevent,
    ora_dict_obj_type,
    ora_dict_obj_name,
    SUBSTR(ora_sql_txt_to_str(), 1, 4000)
  );
END;
/
LOGON/LOGOFFトリガーの例
-- ログオン時の初期化
CREATE OR REPLACE TRIGGER trg_logon_audit
AFTER LOGON ON DATABASE
BEGIN
  INSERT INTO logon_audit(
    login_time, username, machine, program, ip_address
  ) VALUES (
    SYSTIMESTAMP,
    SYS_CONTEXT('USERENV', 'SESSION_USER'),
    SYS_CONTEXT('USERENV', 'HOST'),
    SYS_CONTEXT('USERENV', 'MODULE'),
    SYS_CONTEXT('USERENV', 'IP_ADDRESS')
  );
END;
/

-- 監査は自律トランザクションでCOMMIT推奨(ログイン失敗でもログ残す)

System Triggerの種類

  • DDL系:CREATE/ALTER/DROP/TRUNCATE/GRANT/REVOKE
  • Database系:LOGON/LOGOFF/STARTUP/SHUTDOWN/SERVERERROR
  • スコープON SCHEMA(そのスキーマのみ)/ON DATABASE(全体)
  • Ora_*属性関数ora_syseventora_dict_obj_typeora_dict_obj_nameで詳細取得

実務実装5パターン

①監査ログ自動記録

INSERT/UPDATE/DELETEを全記録
-- 監査ログテーブル
CREATE TABLE audit_log(
  id          NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  table_name  VARCHAR2(30),
  action      VARCHAR2(10),
  row_id      NUMBER,
  old_data    CLOB,
  new_data    CLOB,
  changed_by  VARCHAR2(100),
  changed_at  TIMESTAMP DEFAULT SYSTIMESTAMP
);

-- トリガー
CREATE OR REPLACE TRIGGER trg_orders_audit
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW
DECLARE
  v_action VARCHAR2(10);
  v_old    CLOB;
  v_new    CLOB;
  v_id     NUMBER;
BEGIN
  IF INSERTING THEN
    v_action := 'INSERT';
    v_id := :NEW.id;
    v_new := 'amount=' || :NEW.amount || ',status=' || :NEW.status;
  ELSIF UPDATING THEN
    v_action := 'UPDATE';
    v_id := :NEW.id;
    v_old := 'amount=' || :OLD.amount || ',status=' || :OLD.status;
    v_new := 'amount=' || :NEW.amount || ',status=' || :NEW.status;
  ELSE
    v_action := 'DELETE';
    v_id := :OLD.id;
    v_old := 'amount=' || :OLD.amount || ',status=' || :OLD.status;
  END IF;

  INSERT INTO audit_log(table_name, action, row_id, old_data, new_data, changed_by)
  VALUES ('ORDERS', v_action, v_id, v_old, v_new,
          SYS_CONTEXT('USERENV', 'SESSION_USER'));
END;
/

②updated_at自動更新

CREATE OR REPLACE TRIGGER trg_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
  :NEW.updated_at := SYSTIMESTAMP;
  -- 作成日時は不変(上書きされない保護)
  :NEW.created_at := :OLD.created_at;
END;

③楽観的ロック(バージョン列)

CREATE OR REPLACE TRIGGER trg_users_version
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
  -- バージョン列を自動インクリメント
  :NEW.version := NVL(:OLD.version, 0) + 1;

  -- アプリ層から渡された期待バージョンと照合
  -- (アプリは UPDATE ... WHERE version = :expected で更新)
END;

④データ整合性の強制

禁止値/状態遷移チェック
CREATE OR REPLACE TRIGGER trg_orders_validate
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW
BEGIN
  -- 金額マイナス禁止
  IF :NEW.amount &lt; 0 THEN
    RAISE_APPLICATION_ERROR(-20101, '金額はマイナス不可: ' || :NEW.amount);
  END IF;

  -- 状態遷移ルール
  IF UPDATING THEN
    IF :OLD.status = 'closed' AND :NEW.status != 'closed' THEN
      RAISE_APPLICATION_ERROR(-20102, 'closed状態から復帰できません');
    END IF;

    IF :OLD.status = 'shipped' AND :NEW.status = 'pending' THEN
      RAISE_APPLICATION_ERROR(-20103, 'shipped→pendingは不可');
    END IF;
  END IF;
END;

⑤非正規化データの自動同期

集計列の自動更新
-- ordersの合計金額をusers.total_spentに反映
CREATE OR REPLACE TRIGGER trg_update_user_total
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW
BEGIN
  IF INSERTING OR UPDATING THEN
    UPDATE users
    SET total_spent = total_spent + :NEW.amount - NVL(:OLD.amount, 0)
    WHERE id = :NEW.user_id;
  ELSE
    UPDATE users
    SET total_spent = total_spent - :OLD.amount
    WHERE id = :OLD.user_id;
  END IF;
END;

パターン⑤は要注意:非正規化データの同期は便利ですが、①バッチ処理で大量DMLすると重い、②複雑な計算はマテリアライズドビューの方が良い、③初期データ投入時にトリガーが動くと困る(ALTER TRIGGER DISABLEで一時無効化)。

トリガーの管理:無効化・有効化・削除

トリガーのメンテナンス
-- 一時無効化(大量データ投入時など)
ALTER TRIGGER trg_orders_audit DISABLE;

-- 一括無効化(テーブル単位)
ALTER TABLE orders DISABLE ALL TRIGGERS;

-- 再有効化
ALTER TRIGGER trg_orders_audit ENABLE;
ALTER TABLE orders ENABLE ALL TRIGGERS;

-- 削除
DROP TRIGGER trg_orders_audit;

-- 既存トリガー一覧(現ユーザー)
SELECT trigger_name, trigger_type, triggering_event,
       table_name, status
FROM user_triggers
WHERE table_name = 'ORDERS';

-- ソース確認
SELECT text FROM user_source
WHERE name = 'TRG_ORDERS_AUDIT' AND type = 'TRIGGER'
ORDER BY line;

大量データ投入時のベストプラクティス:ALTER TABLE ... DISABLE ALL TRIGGERSで全トリガーを無効化、②大量INSERT/UPDATE実行、③ENABLE ALL TRIGGERSで再有効化、④必要なら手動で集計値・監査ログを整合。1万件×トリガー50ms=8分強かかる処理が、無効化で1分以内になることも。

アンチパターン7選

①トリガーにビジネスロジックを詰め込む。「注文が入ったらポイント計算→メール送信→在庫減算→通知」等を全部トリガーで書くと、デバッグ困難+テスト不能+性能悪化。ビジネスロジックはアプリ層かストアドプロシージャに。

②Row Triggerで同テーブルをSELECT。Mutating Table例外(ORA-04091)発生。Compound Triggerに変更するかBEFORE STATEMENTで事前取得する。

③トリガー内でCOMMIT/ROLLBACK。Row Trigger内でCOMMITするとORA-04092: cannot COMMIT in a triggerエラー。どうしても必要ならPRAGMA AUTONOMOUS_TRANSACTIONで独立化。

④連鎖トリガー(トリガーA→テーブルB更新→トリガーB→テーブルC更新→…)。循環すると無限ループ的な動作になり、処理追跡が困難。トリガーは1階層までに抑えるルールを設定。

WHEN OTHERS THEN NULL;で例外握り潰し。トリガー内の例外がサイレント失敗→データ不整合発生。ログ記録+RAISEで呼び出し元に伝える。

⑥重いSELECTをトリガー内で実行。1万件INSERT時に各行で0.5秒のSELECTが走ると50分かかる。Compound TriggerでBEFORE STATEMENTに集計を持ち上げる。

⑦トリガー無しでもできる処理をトリガー化DEFAULT SYSTIMESTAMPで済むcreated_atIDENTITY列で済むシーケンス採番をわざわざトリガーで実装しない。制約やDEFAULT値で表現できるならそちらを優先。

よくある質問

QBEFOREとAFTERどちらを使うべき?
A値を書き換えたい→BEFORE、確定後の通知/ログ→AFTER:NEW.colへの代入はBEFORE ROWでのみ可能。AFTER ROWは:NEW:OLDを参照できますが書き換え不可。性能面での差はほぼ無いので、「何をしたいか」で選びます。
QRow TriggerとStatement Triggerの違いは?
ARow Trigger(FOR EACH ROWは対象行ごとに発火、:NEW:OLDが使える。Statement Trigger(デフォルト)はDML文全体で1回、:NEW:OLDは使えず、INSERTINGUPDATING等の述語のみ。1万件一括UPDATEでStatement Triggerは1回、Row Triggerは1万回発火します。
QMutating Table例外(ORA-04091)の対処法は?
ACompound TriggerでBEFORE STATEMENTに事前SELECTを持ち上げる(推奨)、②PRAGMA AUTONOMOUS_TRANSACTIONで独立トランザクション化(副作用に注意)、③Row Trigger→Statement Triggerへの分離(レガシーパターン)。現代はCompound Trigger一択。本記事「Mutating Table問題」セクション参照。
Q:NEW.colをBEFOREで書き換えられるのはなぜ?
ABEFOREトリガーはDML実行前に動作するため、実際にテーブルに書き込まれる前の「候補値」を変更できます。AFTERトリガーは既に書き込み後なので変更不可(コンパイルエラー)。デフォルト値補完・正規化・検証+補正はBEFOREで行うのが定石。
Q1つのトリガーで INSERT/UPDATE/DELETE を全部扱うには?
ABEFORE INSERT OR UPDATE OR DELETE ON テーブルと書き、トリガー本体でIF INSERTING THEN ... ELSIF UPDATING THEN ... ELSE DELETING END IF;で分岐します。UPDATING('列名')特定列の更新判定も可能。
QWHEN句とトリガー本体のIFは何が違う?
AWHEN句は発火前に評価されFalseならトリガー本体が実行されません(軽量)。本体のIFは発火後に評価されるため、発火コスト自体は発生。条件分岐で発火しない場合が多いならWHEN句で絞る方が高速
Qトリガーをテスト/デバッグする方法は?
ADBMS_OUTPUT.PUT_LINE(SERVEROUTPUT ONで有効化)、②ALTER TRIGGER ... DEBUG COMPILEでデバッグビルド、③PRAGMA AUTONOMOUS_TRANSACTION付きのログ用プロシージャで確実に記録、④USER_ERRORSでコンパイルエラー詳細確認。トリガー例外処理は【PL/SQL】例外処理完全ガイドを参照。
QCompound Triggerの状態変数はいつリセットされる?
Aトリガー発火の都度リセットされます(BEFORE STATEMENTで初期化するのが定石)。複数のDML間では共有されず、1つのDML文の4フェーズ間でのみ状態保持。複数DML間で共有したい場合はパッケージ変数を使います。
QビューにUPDATEしたら「more than one base table」エラーが出た
AJOINビューは通常更新不可(ORA-01776)。INSTEAD OFトリガーを定義すると、ビューへのDMLを「代替処理」(複数テーブルへの個別DML)に置き換えられます。本記事「INSTEAD OFトリガー」セクション参照。
Qトリガーが発火しない
AUSER_TRIGGERSSTATUSDISABLEDになっていないか確認、②WHEN句の条件がNULLで常にFALSEになっていないか、③対象のUPDATE OF 列名と違う列を更新していないか、④コンパイルエラーでINVALIDになっていないか(USER_ERRORS)。インサート文にAPPENDヒントを付けるとトリガーが無効化されるケースもあります。
Q複数トリガーの発火順は決まっている?
AOracleは原則作成順ですが、12c以降はFOLLOWSPRECEDES句で明示可能:CREATE OR REPLACE TRIGGER trg_b BEFORE UPDATE ON t FOR EACH ROW FOLLOWS trg_a ...同じテーブル・同じタイミングに複数のトリガーがあるなら発火順を明示する方が保守的。
Qトリガー本体のパフォーマンス最適化のコツは?
AWHEN句で絞る(発火回数そのものを減らす)、②Compound Triggerで集計を1回に(BEFORE STATEMENTに寄せる)、③重いSQLを避ける(Mutating Tableも起きやすい)、④大量DML前はDISABLEで停止、⑤ビジネスロジックはアプリ層に寄せ、トリガーはデータ整合性維持だけに絞る。

関連記事

まとめ

  • トリガー6種類:Row/Statement/INSTEAD OF/Compound/System (DDL)/System (Database)
  • DMLトリガー4象限:BEFORE/AFTER × Row/Statementで用途分け
  • :NEW書き換え可能なのはBEFORE ROWのみ
  • INSERTINGUPDATINGDELETING述語とUPDATING('列名')で分岐
  • WHEN (条件)句で発火コストそのものを削減
  • Compound Trigger(11g+)で4フェーズ制御+状態共有+Mutating Table回避
  • Mutating Table例外(ORA-04091)対策はCompound Trigger一択(古いパッケージ状態パターンより明確)
  • INSTEAD OF TriggerでJOINビューを更新可能に
  • System Trigger:DDL監査・LOGON監査でスキーマ全体の見守り
  • 実務5パターン:監査ログ/updated_at/楽観的ロック/整合性チェック/非正規化同期
  • 大量DML前はALTER TABLE ... DISABLE ALL TRIGGERSで一時無効化
  • アンチパターン:ビジネスロジック肥大化/同テーブルSELECT/COMMIT/連鎖/例外握り潰し/重いSELECT/DEFAULT代替可能

トリガーは「データの副作用をDB内に閉じ込める」強力な仕組みですが、使いどころを間違えるとデバッグ困難+性能悪化の温床になります。本記事の6種類モデル・Compound Trigger・Mutating Table対策・5実務パターン・7アンチパターンを押さえれば、必要な場面でだけトリガーを使い、必要ない場面では制約/DEFAULT/アプリ層で代替する判断ができるようになります。例外処理は例外処理完全ガイド、自律トランザクションはAUTONOMOUS TRANSACTION、パッケージはパッケージ管理と組み合わせて活用してください。