【Oracle】トリガー作成・高度な活用完全ガイド|複合トリガー・DDLトリガー・WHEN句・自律トランザクション・パフォーマンス注意点まで解説

【Oracle】トリガー作成・高度な活用完全ガイド|複合トリガー・DDLトリガー・WHEN句・自律トランザクション・パフォーマンス注意点まで解説 Oracle

Oracle のトリガーは INSERT / UPDATE / DELETE に反応する DML トリガーだけではありません。CREATE TABLE や DROP TABLE などの DDL イベントに反応する DDL トリガーや、ログオン・ログオフ・DB 起動・停止に反応するシステムトリガーも存在します。

また、複合トリガー(Compound Trigger)を使えば、1 つのトリガー定義で BEFORE/AFTER × 文レベル/行レベルの 4 つのタイミングをまとめて扱えます。大量 DML での性能問題が起きやすい「1 行ごとにコレクションへ追記 → AFTER STATEMENT で一括処理」というパターンも複合トリガーなしでは実装が難しい内容です。

この記事では、複合トリガー・DDL/システムトリガー・WHEN 句・OF 句・FOLLOWS による発火制御、自律トランザクションでのログ記録など、基本的な CREATE TRIGGER の先にある実務知識を体系的に解説します。

この記事でわかること
・複合トリガー(Compound Trigger)の構造と「一括処理」パターン
・DDL トリガー:CREATE / DROP / ALTER イベントの監視
・システムイベントトリガー:LOGON / LOGOFF / STARTUP / SHUTDOWN の捕捉
・WHEN 句:特定条件を満たす行のみトリガーを発火させる方法
・OF 列名:特定列が UPDATE されたときだけ発火させる方法
・FOLLOWS:同じテーブルに複数トリガーがある場合の実行順序制御
・PRAGMA AUTONOMOUS_TRANSACTION:ロールバックされてもログを残す方法
・トリガーがパフォーマンスに与える影響と設計上の注意点
・ORA-04091(ミューテーティングテーブル)の原因と回避策
トリガーの基本(CREATE/BEFORE/AFTER/行レベル/文レベル/:NEW/:OLD)やUSER_TRIGGERS / ALL_TRIGGERS / DBA_TRIGGERS でのトリガー情報確認、有効化・無効化(ENABLE / DISABLE)については【Oracle】トリガー情報を取得するSQL文完全解説を参照してください。この記事はその続編として、より高度な機能と実務パターンを扱います。
スポンサーリンク

Oracle トリガーの全種類

種類 発火タイミング 主な用途
DML トリガー(BEFORE/AFTER) INSERT / UPDATE / DELETE の前後 採番・監査ログ・整合性チェック・派生列更新
INSTEAD OF トリガー ビューへの DML 更新不可ビューを通じて基底テーブルを操作する
複合トリガー(Compound) DML の 4 タイミングすべて(1 定義内) 行レベルでデータ収集し文レベルで一括処理する
DDL トリガー CREATE / ALTER / DROP / RENAME 等 スキーマ変更の監査・禁止ルール適用
データベース・スキーマトリガー LOGON / LOGOFF / STARTUP / SHUTDOWN / ERROR ログイン制限・セッション情報の記録・DB 起動処理

複合トリガー(Compound Trigger)

Oracle 11g で導入された複合トリガーは、1 つのトリガー定義の中に 4 つのタイミングセクションを持つ特殊なトリガーです。各セクション間でパッケージ変数のようにデータを共有できます。

セクション 実行タイミング 用途
BEFORE STATEMENT 文全体の実行前(1回) 処理開始時の初期化・ロック取得
BEFORE EACH ROW 各行の処理前 :NEW の値の書き換え・採番
AFTER EACH ROW 各行の処理後 行データをコレクションに蓄積
AFTER STATEMENT 文全体の実行後(1回) 蓄積データを一括 INSERT・一括処理
SQL(複合トリガーの構造)
CREATE OR REPLACE TRIGGER trg_emp_compound
    FOR INSERT OR UPDATE OR DELETE ON emp
    COMPOUND TRIGGER

    -- セクション間で共有するコレクション(パッケージ変数的な役割)
    TYPE t_log_rec IS RECORD (
        emp_id   emp.emp_id%TYPE,
        action   VARCHAR2(10),
        changed  TIMESTAMP
    );
    TYPE t_log_tab IS TABLE OF t_log_rec INDEX BY PLS_INTEGER;
    g_logs  t_log_tab;
    g_idx   PLS_INTEGER := 0;

    -- ① 文レベルの前処理
    BEFORE STATEMENT IS
    BEGIN
        g_idx  := 0;
        g_logs.DELETE;
    END BEFORE STATEMENT;

    -- ② 行ごとにログデータを収集(INSERT は重くない)
    AFTER EACH ROW IS
    BEGIN
        g_idx := g_idx + 1;
        g_logs(g_idx).emp_id  := CASE WHEN DELETING THEN :OLD.emp_id ELSE :NEW.emp_id END;
        g_logs(g_idx).action  := CASE WHEN INSERTING THEN 'INSERT'
                                       WHEN UPDATING  THEN 'UPDATE'
                                       ELSE                'DELETE' END;
        g_logs(g_idx).changed := SYSTIMESTAMP;
    END AFTER EACH ROW;

    -- ③ 全行処理後に一括 INSERT(ループ数が少ないため高速)
    AFTER STATEMENT IS
    BEGIN
        FORALL i IN 1 .. g_idx
            INSERT INTO emp_audit (emp_id, action, changed_at)
            VALUES (g_logs(i).emp_id, g_logs(i).action, g_logs(i).changed);
    END AFTER STATEMENT;

END trg_emp_compound;
/
複合トリガーが必要な代表的ケース
通常の AFTER EACH ROW トリガーで 1 行ごとに別テーブルへ INSERT すると、10 万行の UPDATE に対して 10 万回の INSERT が走り極めて遅くなります。複合トリガーを使えば AFTER EACH ROW でコレクションに蓄積し、AFTER STATEMENT で FORALL による一括 INSERT にまとめられます。この「バルク化」により数十倍の性能改善が見込めます。
複合トリガーの制限
・INSTEAD OF トリガーには複合トリガーを使えない(ビューには 4 タイミングが存在しないため)
・セクション内から同じトリガーのテーブルを直接操作するとミューテーティングエラーになる(後述)
・各セクションは独立した例外ハンドラを持てない(トリガー本体全体で1つ)

DDL トリガー:スキーマ変更を監視する

DDL トリガーは CREATE TABLEDROP TABLE などの DDL 実行に反応します。本番環境での無断スキーマ変更を検知・禁止する用途に使われます。

SQL(DDL 変更を監査テーブルに記録するトリガー)
-- DDL 変更を記録するテーブル(事前に作成)
CREATE TABLE ddl_audit_log (
    log_id      NUMBER GENERATED ALWAYS AS IDENTITY,
    event_type  VARCHAR2(30),
    object_type VARCHAR2(30),
    object_name VARCHAR2(128),
    schema_name VARCHAR2(128),
    db_user     VARCHAR2(128),
    os_user     VARCHAR2(128),
    event_time  TIMESTAMP DEFAULT SYSTIMESTAMP,
    CONSTRAINT pk_ddl_audit PRIMARY KEY (log_id)
);

-- スキーマ内のすべての DDL を記録するトリガー
CREATE OR REPLACE TRIGGER trg_ddl_audit
    AFTER DDL ON SCHEMA
BEGIN
    INSERT INTO ddl_audit_log (
        event_type, object_type, object_name, schema_name,
        db_user, os_user
    ) VALUES (
        ORA_SYSEVENT,
        ORA_DICT_OBJ_TYPE,
        ORA_DICT_OBJ_NAME,
        ORA_DICT_OBJ_OWNER,
        SYS_CONTEXT('USERENV', 'SESSION_USER'),
        SYS_CONTEXT('USERENV', 'OS_USER')
    );
END;
/
SQL(特定の DDL を禁止するトリガー)
-- 本番スキーマでの DROP TABLE を禁止する
CREATE OR REPLACE TRIGGER trg_no_drop
    BEFORE DROP ON SCHEMA
BEGIN
    RAISE_APPLICATION_ERROR(-20001, 'この環境では DROP TABLE は禁止されています');
END;
/
DDL イベント 発火タイミング 主な用途
CREATE 任意のオブジェクト作成後 新規オブジェクトの作成を記録
ALTER オブジェクト変更後 テーブル定義変更の記録
DROP オブジェクト削除前後 本番での誤削除防止・記録
RENAME オブジェクト名変更後 名前変更の追跡
TRUNCATE テーブル全件削除前後 本番での誤 TRUNCATE 防止
GRANT / REVOKE 権限付与・剥奪後 セキュリティ変更の監査
DDL トリガー内で使える属性関数
ORA_SYSEVENT:発生したイベント名(CREATE / DROP など)
ORA_DICT_OBJ_TYPE:オブジェクト種別(TABLE / INDEX など)
ORA_DICT_OBJ_NAME:オブジェクト名
ORA_DICT_OBJ_OWNER:オブジェクトのオーナースキーマ
ORA_LOGIN_USER:DDL を実行したユーザー

システムイベントトリガー:ログオン・DB起動を捕捉する

システムイベントトリガーはデータベースレベルのイベント(ログオン・ログオフ・起動・停止)に反応します。主に DBA が使いますが、セキュリティ監視やセッション初期化に有効です。

SQL(LOGON トリガー:ログイン情報を記録)
-- ログイン記録テーブル(事前に作成)
CREATE TABLE logon_audit (
    session_id  NUMBER,
    db_user     VARCHAR2(128),
    os_user     VARCHAR2(128),
    ip_address  VARCHAR2(40),
    logon_time  TIMESTAMP DEFAULT SYSTIMESTAMP
);

-- データベース全体へのログオンを記録
CREATE OR REPLACE TRIGGER trg_logon_audit
    AFTER LOGON ON DATABASE
DECLARE
    PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
    INSERT INTO logon_audit (session_id, db_user, os_user, ip_address)
    VALUES (
        SYS_CONTEXT('USERENV', 'SESSIONID'),
        SYS_CONTEXT('USERENV', 'SESSION_USER'),
        SYS_CONTEXT('USERENV', 'OS_USER'),
        SYS_CONTEXT('USERENV', 'IP_ADDRESS')
    );
    COMMIT;
END;
/
SQL(特定ユーザーのログオンを禁止するトリガー)
-- SCOTT ユーザーのログオンを禁止(例:セキュリティポリシー)
CREATE OR REPLACE TRIGGER trg_deny_scott
    AFTER LOGON ON DATABASE
BEGIN
    IF SYS_CONTEXT('USERENV', 'SESSION_USER') = 'SCOTT' THEN
        RAISE_APPLICATION_ERROR(-20002, 'このユーザーのログオンは禁止されています');
    END IF;
END;
/
システムイベント 発火タイミング 主な用途
STARTUP DB 起動直後 起動時の初期化処理・リソース確保
SHUTDOWN DB 停止前 終了前のクリーンアップ処理
LOGON ユーザーログオン後 セッション情報の記録・ログイン制限
LOGOFF ユーザーログオフ前 セッション終了の記録
SERVERERROR エラー発生後 エラーの自動記録・通知

発火条件の制御:WHEN 句・OF 句・FOLLOWS

WHEN 句:特定条件を満たす行のみ発火させる

WHEN 句を使うと、すべての行ではなく特定の条件を満たす行に対してのみトリガーを発火させられます。WHEN 句は行レベルトリガー(FOR EACH ROW)のみで使えます。条件を絞ることで不要な処理を省き、パフォーマンスを改善できます。

SQL(WHEN 句:給与が100万円以上の行のみ発火)
CREATE OR REPLACE TRIGGER trg_high_salary_log
    AFTER UPDATE OF salary ON emp
    FOR EACH ROW
    WHEN (NEW.salary >= 1000000)   -- :NEW ではなく NEW と書く(WHEN 句は特別)
BEGIN
    INSERT INTO salary_change_log (emp_id, old_sal, new_sal, changed_at)
    VALUES (:OLD.emp_id, :OLD.salary, :NEW.salary, SYSTIMESTAMP);
END;
/
WHEN 句では :NEW / :OLD ではなく NEW / OLD を使う
WHEN 句の中だけは :NEW ではなく NEW(コロンなし)と記述します。BEGIN … END ブロック内は通常どおり :NEW と書きます。この違いを混同するとコンパイルエラーになります。

OF 列名:特定の列が更新されたときだけ発火させる

UPDATE OF 列名 を指定すると、指定した列が実際に変更された UPDATE の場合のみトリガーが発火します。条件述語 UPDATING('列名') と組み合わせると、どの列が更新されたかを判定できます。

SQL(OF 句:salary または dept_id の変更のみ発火)
CREATE OR REPLACE TRIGGER trg_emp_key_change
    AFTER UPDATE OF salary, dept_id ON emp
    FOR EACH ROW
BEGIN
    IF UPDATING('SALARY') THEN
        INSERT INTO emp_change_log (emp_id, change_type, old_val, new_val)
        VALUES (:NEW.emp_id, 'SALARY', TO_CHAR(:OLD.salary), TO_CHAR(:NEW.salary));
    END IF;
    IF UPDATING('DEPT_ID') THEN
        INSERT INTO emp_change_log (emp_id, change_type, old_val, new_val)
        VALUES (:NEW.emp_id, 'DEPT_ID', TO_CHAR(:OLD.dept_id), TO_CHAR(:NEW.dept_id));
    END IF;
END;
/

FOLLOWS:複数トリガーの実行順序を指定する

同じテーブルの同じタイミングに複数のトリガーが定義されている場合、デフォルトでは実行順序は不定です。FOLLOWS を指定するとトリガーの依存関係を明示できます(Oracle 11g 以降)。

SQL(FOLLOWS:トリガーの実行順序を指定)
-- trg_emp_validate が先に実行されてから trg_emp_audit が実行される
CREATE OR REPLACE TRIGGER trg_emp_audit
    AFTER INSERT ON emp
    FOR EACH ROW
    FOLLOWS trg_emp_validate    -- trg_emp_validate の後に実行
BEGIN
    INSERT INTO emp_audit (emp_id, action) VALUES (:NEW.emp_id, 'INSERT');
END;
/

PRAGMA AUTONOMOUS_TRANSACTION:ロールバックされてもログを残す

トリガーは呼び出し元のトランザクションと同じトランザクション内で動作します。つまり、呼び出し元がロールバックするとトリガー内の INSERT も一緒にロールバックされます

エラーが発生してロールバックされた場合でもエラーログを残したい場合は、PRAGMA AUTONOMOUS_TRANSACTION独立したトランザクションとしてトリガーを実行します。

SQL(自律トランザクション:ロールバックされてもログを残す)
CREATE OR REPLACE TRIGGER trg_emp_error_log
    BEFORE INSERT OR UPDATE ON emp
    FOR EACH ROW
DECLARE
    PRAGMA AUTONOMOUS_TRANSACTION;  -- 自律トランザクション宣言
BEGIN
    -- このトリガー内の処理は独立したトランザクション
    INSERT INTO emp_dml_log (
        action, emp_id, executed_by, log_time
    ) VALUES (
        CASE WHEN INSERTING THEN 'INSERT' ELSE 'UPDATE' END,
        :NEW.emp_id,
        SYS_CONTEXT('USERENV', 'SESSION_USER'),
        SYSTIMESTAMP
    );
    COMMIT;  -- 自律トランザクションは必ず COMMIT または ROLLBACK で終了させる
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        -- ログ書き込み失敗は無視して本体処理を継続させる
END;
/
PRAGMA AUTONOMOUS_TRANSACTION を使う際の注意点
① 自律トランザクション内では必ず COMMIT または ROLLBACK で終了すること。終了しないと ORA-06519: アクティブな自立したトランザクションが検出されました エラーになります。
② 自律トランザクションは親トランザクションの変更をコミット前には見えません(READ COMMITTED の隔離)。
③ ロックは親トランザクションと独立して管理されます。親がロックしている行を自律トランザクション内で更新するとデッドロックになります。

ORA-04091:ミューテーティングテーブルエラーの原因と回避策

行レベルトリガーの中から、トリガーが設定されている同じテーブルを SELECT または DML するとORA-04091: テーブル … は変化中です エラーが発生します。これは Oracle がデータの一貫性を守るための制約です。

SQL(NG:ミューテーティングテーブルエラーになる例)
-- NG: emp 行レベルトリガー内で emp を SELECT している
CREATE OR REPLACE TRIGGER trg_emp_mutating  -- このトリガーは ORA-04091 を起こす
    AFTER INSERT ON emp
    FOR EACH ROW
DECLARE
    v_cnt NUMBER;
BEGIN
    -- 同じ emp テーブルを参照 → ORA-04091
    SELECT COUNT(*) INTO v_cnt FROM emp WHERE dept_id = :NEW.dept_id;
END;
/
SQL(OK:複合トリガーで回避する)
-- OK: 複合トリガーの AFTER STATEMENT セクションで集計する
CREATE OR REPLACE TRIGGER trg_emp_dept_check
    FOR INSERT ON emp
    COMPOUND TRIGGER

    TYPE dept_tab IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
    g_dept_ids dept_tab;
    g_idx PLS_INTEGER := 0;

    AFTER EACH ROW IS
    BEGIN
        g_idx := g_idx + 1;
        g_dept_ids(g_idx) := :NEW.dept_id;
    END AFTER EACH ROW;

    AFTER STATEMENT IS
        v_cnt NUMBER;
    BEGIN
        -- INSERT 完了後の AFTER STATEMENT ではテーブルを参照できる
        FOR i IN 1 .. g_idx LOOP
            SELECT COUNT(*) INTO v_cnt FROM emp WHERE dept_id = g_dept_ids(i);
            IF v_cnt > 100 THEN
                RAISE_APPLICATION_ERROR(-20003, '部署の人数上限を超えています');
            END IF;
        END LOOP;
    END AFTER STATEMENT;

END trg_emp_dept_check;
/
ORA-04091 の主な回避策
複合トリガーの AFTER STATEMENT セクションでテーブル参照を行う(推奨)
パッケージ変数に行ごとのデータを蓄積し、AFTER STATEMENT で処理する
③ 参照先が同じテーブルでないなら問題なし(別テーブルの参照は OK)

トリガーがパフォーマンスに与える影響と設計注意点

問題パターン 影響 対処法
行レベルトリガーで 1 行ごとに INSERT 100 万行の UPDATE → 100 万回の INSERT が走り極遅 複合トリガーで AFTER EACH ROW にコレクション蓄積 → AFTER STATEMENT で FORALL 一括 INSERT
トリガー内で重い SELECT や関数呼び出し DML のたびに重い処理が走る 処理を軽量に保つ。複雑なロジックはバッチ処理に分離
AFTER EACH ROW でのコミット(自律 TX) 大量コミットによるログ肥大化・I/O 増加 自律 TX は必要最小限に。ロギングテーブルをパーティション化して管理
トリガーの連鎖(カスケードトリガー) トリガーが別のトリガーを起動し、最大 32 段まで連鎖可能。デバッグ困難 連鎖を避ける設計にする。どうしても必要な場合は FOLLOWS で順序を明示
エラー処理なしのトリガー トリガー内の例外がそのまま呼び出し元に伝播し DML が失敗する EXCEPTION ブロックで適切に処理する
「とりあえずトリガーで」という設計を避ける
トリガーは透明性が低く、アプリケーション開発者が知らない処理がDBレベルで動くためデバッグ・テストが難しくなります。
・監査ログ・自動採番・派生列更新など DB 層で保証すべき処理はトリガーが適切
・ビジネスロジックはアプリケーション層に持たせ、トリガーに複雑なロジックを入れない
・大量 DML が走るテーブルのトリガーは必ずパフォーマンステストを実施する

トリガー作成に必要な権限

種類 必要な権限
自スキーマのテーブルに DML トリガーを作成 CREATE TRIGGER
他スキーマのテーブルに DML トリガーを作成 CREATE ANY TRIGGER
DDL トリガーを作成 CREATE ANY TRIGGER(SCHEMA レベル)/ ADMINISTER DATABASE TRIGGER(DATABASE レベル)
システムイベントトリガーを作成 ADMINISTER DATABASE TRIGGER
権限の確認・付与方法の詳細は【Oracle】ユーザ権限を確認する方法完全ガイドを参照してください。

よくある質問

複合トリガーと通常の BEFORE/AFTER トリガーはどう使い分けますか?
① 同じテーブルへの DML で複数タイミングの処理をまとめたい場合
② 行レベルで収集したデータを文レベルで一括処理したい場合(大量 DML のパフォーマンス最適化)
③ ORA-04091 ミューテーティングエラーを回避したい場合
このいずれかに当てはまれば複合トリガーを使います。単純な 1 タイミングの処理は通常のトリガーで十分です。
DDL トリガーでエラーをスローするとどうなりますか?
BEFORE DDL トリガーで RAISE_APPLICATION_ERROR を投げると、その DDL はキャンセルされます。AFTER DDL トリガーでエラーを投げた場合も、DDL はロールバックされます。これを利用して「本番スキーマでの DROP TABLE を禁止する」などのポリシー適用が可能です。
LOGON トリガーでエラーが起きると誰もログオンできなくなりますか?
はい、LOGON トリガーでエラーが発生すると該当ユーザーがログオンできなくなります。SYS ユーザーは SYSDBA 権限で接続(CONNECT / AS SYSDBA)すればトリガーをバイパスして接続できます。SYS で接続してトリガーを DISABLE または DROP してください。
WHEN 句に :NEW.列名 と書いたらエラーになります
WHEN 句内では :NEW ではなく NEW(コロンなし)を使います。例: WHEN (NEW.salary >= 1000000)。BEGIN … END ブロック内は通常どおり :NEW です。
PRAGMA AUTONOMOUS_TRANSACTION を使うと何が変わりますか?
トリガー内の DML が呼び出し元のトランザクションから独立し、呼び出し元がロールバックしてもトリガー内でコミットした内容は残ります。エラーログ・監査ログを「処理が失敗しても必ず残したい」場合に使います。自律トランザクション内では必ず COMMIT か ROLLBACK を実行してください(ORA-06519 防止)。
同じテーブルに複数のトリガーがあります。実行順序を保証できますか?
Oracle 11g 以降では FOLLOWS トリガー名 を使って実行順序を指定できます。例: CREATE OR REPLACE TRIGGER trg_B ... FOLLOWS trg_A とすると trg_A が先に実行されます。11g 未満では実行順序は不定のため、依存関係がある処理は 1 つのトリガーにまとめることを推奨します。

まとめ

Oracle トリガーの高度な機能をまとめます。

やりたいこと 使う機能
行レベルで収集・文レベルで一括処理する 複合トリガー(AFTER EACH ROW + AFTER STATEMENT + FORALL)
ORA-04091 ミューテーティングを回避する 複合トリガーの AFTER STATEMENT セクションで処理
CREATE/DROP/ALTER などの DDL を監視・禁止する DDL トリガー(BEFORE/AFTER DDL ON SCHEMA)
ログオン・ログオフを記録・制限する システムイベントトリガー(AFTER LOGON ON DATABASE)
特定条件の行だけトリガーを発火させる WHEN 句(コロンなしの NEW/OLD を使う)
特定列の更新のみ発火させる AFTER UPDATE OF 列名 ON テーブル名
複数トリガーの順序を固定する FOLLOWS トリガー名(Oracle 11g 以降)
ロールバックされてもログを残す PRAGMA AUTONOMOUS_TRANSACTION + COMMIT

基本的なトリガー(BEFORE/AFTER の CREATE・ENABLE/DISABLE・監査パターン)については【Oracle】トリガー情報を取得するSQL文完全解説を参照してください。