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(ミューテーティングテーブル)の原因と回避策
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・一括処理 |
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 TABLE や DROP TABLE などの 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;
/
-- 本番スキーマでの 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 | 権限付与・剥奪後 | セキュリティ変更の監査 |
・
ORA_SYSEVENT:発生したイベント名(CREATE / DROP など)・
ORA_DICT_OBJ_TYPE:オブジェクト種別(TABLE / INDEX など)・
ORA_DICT_OBJ_NAME:オブジェクト名・
ORA_DICT_OBJ_OWNER:オブジェクトのオーナースキーマ・
ORA_LOGIN_USER:DDL を実行したユーザーシステムイベントトリガー:ログオン・DB起動を捕捉する
システムイベントトリガーはデータベースレベルのイベント(ログオン・ログオフ・起動・停止)に反応します。主に DBA が使いますが、セキュリティ監視やセッション初期化に有効です。
-- ログイン記録テーブル(事前に作成)
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;
/
-- 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)のみで使えます。条件を絞ることで不要な処理を省き、パフォーマンスを改善できます。
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 ではなく NEW(コロンなし)と記述します。BEGIN … END ブロック内は通常どおり :NEW と書きます。この違いを混同するとコンパイルエラーになります。OF 列名:特定の列が更新されたときだけ発火させる
UPDATE OF 列名 を指定すると、指定した列が実際に変更された UPDATE の場合のみトリガーが発火します。条件述語 UPDATING('列名') と組み合わせると、どの列が更新されたかを判定できます。
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 以降)。
-- 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 で独立したトランザクションとしてトリガーを実行します。
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;
/
① 自律トランザクション内では必ず
COMMIT または ROLLBACK で終了すること。終了しないと ORA-06519: アクティブな自立したトランザクションが検出されました エラーになります。② 自律トランザクションは親トランザクションの変更をコミット前には見えません(READ COMMITTED の隔離)。
③ ロックは親トランザクションと独立して管理されます。親がロックしている行を自律トランザクション内で更新するとデッドロックになります。
ORA-04091:ミューテーティングテーブルエラーの原因と回避策
行レベルトリガーの中から、トリガーが設定されている同じテーブルを SELECT または DML するとORA-04091: テーブル … は変化中です エラーが発生します。これは Oracle がデータの一貫性を守るための制約です。
-- 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;
/
-- 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;
/
① 複合トリガーの 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 |
よくある質問
② 行レベルで収集したデータを文レベルで一括処理したい場合(大量 DML のパフォーマンス最適化)
③ ORA-04091 ミューテーティングエラーを回避したい場合
このいずれかに当てはまれば複合トリガーを使います。単純な 1 タイミングの処理は通常のトリガーで十分です。
BEFORE DDL トリガーで RAISE_APPLICATION_ERROR を投げると、その DDL はキャンセルされます。AFTER DDL トリガーでエラーを投げた場合も、DDL はロールバックされます。これを利用して「本番スキーマでの DROP TABLE を禁止する」などのポリシー適用が可能です。CONNECT / AS SYSDBA)すればトリガーをバイパスして接続できます。SYS で接続してトリガーを DISABLE または DROP してください。:NEW ではなく NEW(コロンなし)を使います。例: WHEN (NEW.salary >= 1000000)。BEGIN … END ブロック内は通常どおり :NEW です。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文完全解説を参照してください。

