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 DISABLE/ENABLE/DROPの管理- パフォーマンス最適化とトリガー無効化時のメンテナンス
- アンチパターン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;
/
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種類に分類されます。用途で自動的に選択肢が絞られます。
DMLトリガーの4象限(BEFORE/AFTER × Row/Statement)
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 擬似レコード完全解説
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/:OLDはRow Triggerでのみ使える(Statementでは使えない)、③UPDATING('列名')で特定列の更新判定も可能(IF UPDATING('salary') THEN ...)、④WHEN句内ではコロン無し(NEW.col/OLD.col)。
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.col/OLD.colと書く(トリガー本体とは記法が違う) - NULL比較は必ず
IS NULL(NEW.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問題の解決にも最も簡潔。
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で防いでいます。
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 > 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で分離
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 > 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へ分離
-- パッケージで状態を保持
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を「代替処理」に置き換えられます。
-- 複数テーブルを結合したビュー
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・ログオン監査
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;
/
-- ログオン時の初期化
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_sysevent/ora_dict_obj_type/ora_dict_obj_nameで詳細取得
実務実装5パターン
①監査ログ自動記録
-- 監査ログテーブル
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 < 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_atやIDENTITY列で済むシーケンス採番をわざわざトリガーで実装しない。制約やDEFAULT値で表現できるならそちらを優先。
よくある質問
:NEW.colへの代入はBEFORE ROWでのみ可能。AFTER ROWは:NEW/:OLDを参照できますが書き換え不可。性能面での差はほぼ無いので、「何をしたいか」で選びます。FOR EACH ROW)は対象行ごとに発火、:NEW/:OLDが使える。Statement Trigger(デフォルト)はDML文全体で1回、:NEW/:OLDは使えず、INSERTING/UPDATING等の述語のみ。1万件一括UPDATEでStatement Triggerは1回、Row Triggerは1万回発火します。PRAGMA AUTONOMOUS_TRANSACTIONで独立トランザクション化(副作用に注意)、③Row Trigger→Statement Triggerへの分離(レガシーパターン)。現代はCompound Trigger一択。本記事「Mutating Table問題」セクション参照。BEFORE INSERT OR UPDATE OR DELETE ON テーブルと書き、トリガー本体でIF INSERTING THEN ... ELSIF UPDATING THEN ... ELSE DELETING END IF;で分岐します。UPDATING('列名')で特定列の更新判定も可能。DBMS_OUTPUT.PUT_LINE(SERVEROUTPUT ONで有効化)、②ALTER TRIGGER ... DEBUG COMPILEでデバッグビルド、③PRAGMA AUTONOMOUS_TRANSACTION付きのログ用プロシージャで確実に記録、④USER_ERRORSでコンパイルエラー詳細確認。トリガー例外処理は【PL/SQL】例外処理完全ガイドを参照。USER_TRIGGERSのSTATUSがDISABLEDになっていないか確認、②WHEN句の条件がNULLで常にFALSEになっていないか、③対象のUPDATE OF 列名と違う列を更新していないか、④コンパイルエラーでINVALIDになっていないか(USER_ERRORS)。インサート文にAPPENDヒントを付けるとトリガーが無効化されるケースもあります。FOLLOWS/PRECEDES句で明示可能:CREATE OR REPLACE TRIGGER trg_b BEFORE UPDATE ON t FOR EACH ROW FOLLOWS trg_a ...同じテーブル・同じタイミングに複数のトリガーがあるなら発火順を明示する方が保守的。DISABLEで停止、⑤ビジネスロジックはアプリ層に寄せ、トリガーはデータ整合性維持だけに絞る。関連記事
- 【PL/SQL】IF文完全ガイド — トリガー内のIF分岐とEvent Predicates
- 【PL/SQL】例外処理完全ガイド — RAISE_APPLICATION_ERROR/トリガー例外
- 【PL/SQL】ループ処理完全ガイド — Compound Trigger内のコレクション走査
- 【PL/SQL】カーソル完全ガイド — トリガーから参照するカーソル
- 【PL/SQL】変数・定数完全ガイド — トリガー内のローカル変数
- 【PL/SQL】予約語完全ガイド — TRIGGER/BEFORE/AFTER等のキーワード
- 【PL/SQL】初心者でもわかる基本構文とブロック構造の書き方 — トリガー内のPL/SQLブロック
- 【PL/SQL】ストアドプロシージャとファンクションの違いと作り方 — トリガーからプロシージャ呼び出し
- 【PL/SQL】AUTONOMOUS TRANSACTIONで独立した処理を行う方法 — 自律トランザクションでMutating Table回避
- 【PL/SQL】パッケージを使ったコード管理と再利用性向上 — トリガー間で状態を共有するパッケージ変数
まとめ
- トリガー6種類:Row/Statement/INSTEAD OF/Compound/System (DDL)/System (Database)
- DMLトリガー4象限:BEFORE/AFTER × Row/Statementで用途分け
:NEW書き換え可能なのはBEFORE ROWのみINSERTING/UPDATING/DELETING述語と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、パッケージはパッケージ管理と組み合わせて活用してください。

