Oracle の行レベルトリガー(FOR EACH ROW)では、トリガー発火元のテーブルを参照・変更しようとすると ORA-04091: ミュータルテーブルエラーが発生します。また、大量の DML が発生する場面では行レベルトリガーが行数分だけ起動されてパフォーマンスが低下します。
Oracle 11g で導入されたコンパウンドトリガー(Compound Trigger)は、1 つのトリガーに複数のタイミングポイント(BEFORE STATEMENT / BEFORE EACH ROW /AFTER EACH ROW / AFTER STATEMENT)を定義できます。これによりミュータルテーブルエラーの回避と大量 DML の一括処理を実現できます。
この記事でわかること
- コンパウンドトリガーの構文と 4 つのタイミングポイントの役割
- ORA-04091 ミュータルテーブルエラーをコンパウンドトリガーで解決する方法
- AFTER EACH ROW でデータを収集して AFTER STATEMENT で一括 INSERT する高速化パターン
- コンパウンドトリガー内の変数はステートメント間で共有される特性
- 通常のトリガーとコンパウンドトリガーの使い分け
コンパウンドトリガーの構文
コンパウンドトリガーの基本構文
CREATE OR REPLACE TRIGGER trg_compound_example
FOR INSERT OR UPDATE OR DELETE ON target_table
COMPOUND TRIGGER -- COMPOUND TRIGGER キーワード
-- ===== 共有変数の宣言セクション =====
-- ここで宣言した変数はすべてのタイミングポイントで共有される
-- DML ステートメントの開始時に初期化される
TYPE t_id_list IS TABLE OF target_table.id%TYPE INDEX BY PLS_INTEGER;
g_ids t_id_list; -- 変更された行のIDを収集するコレクション
g_count PLS_INTEGER := 0;
-- ===== タイミングポイント =====
BEFORE STATEMENT IS
BEGIN
-- DML ステートメント実行前に1回だけ実行される
-- 共有変数の初期化に使う
g_ids.DELETE;
g_count := 0;
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
-- 各行の変更前に実行される(通常のBEFORE ROWトリガーと同様)
-- :NEW および :OLD にアクセスできる
NULL; -- ここでは使わない
END BEFORE EACH ROW;
AFTER EACH ROW IS
BEGIN
-- 各行の変更後に実行される(通常のAFTER ROWトリガーと同様)
-- 変更された行の情報を収集する
g_count := g_count + 1;
g_ids(g_count) := :NEW.id;
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
-- DML ステートメント完了後に1回だけ実行される
-- 収集したデータを一括処理する
FOR i IN 1..g_count LOOP
DBMS_OUTPUT.PUT_LINE('変更された ID: ' || g_ids(i));
END LOOP;
END AFTER STATEMENT;
END trg_compound_example;
/
ORA-04091 ミュータルテーブルエラーの解決
行レベルトリガーからトリガー発火元テーブルへのクエリは ORA-04091 を引き起こします。コンパウンドトリガーでは AFTER EACH ROW でキーを収集し、AFTER STATEMENT でステートメント完了後にクエリを実行することで回避できます。
ORA-04091 を解決するコンパウンドトリガー
-- 例: employees テーブルの salary を更新した後、
-- 同じ department の平均給与を department_stats テーブルに反映する
-- 通常のトリガーでは以下がORA-04091を引き起こす:
-- AFTER UPDATE OF salary ON employees FOR EACH ROW
-- BEGIN
-- SELECT AVG(salary) INTO v_avg FROM employees -- ← employees が変更中なのでエラー
-- WHERE department_id = :NEW.department_id;
-- END;
-- コンパウンドトリガーで解決する
CREATE OR REPLACE TRIGGER trg_dept_salary_stats
FOR INSERT OR UPDATE OF salary OR DELETE ON employees
COMPOUND TRIGGER
-- 変更があった department_id を収集するコレクション
TYPE t_dept_ids IS TABLE OF employees.department_id%TYPE INDEX BY PLS_INTEGER;
g_dept_ids t_dept_ids;
g_count PLS_INTEGER := 0;
BEFORE STATEMENT IS
BEGIN
g_dept_ids.DELETE;
g_count := 0;
END BEFORE STATEMENT;
AFTER EACH ROW IS
v_dept_id employees.department_id%TYPE;
BEGIN
-- 変更があった department_id を収集する(重複あり、後でまとめてクエリ)
v_dept_id := COALESCE(:NEW.department_id, :OLD.department_id);
IF v_dept_id IS NOT NULL THEN
g_count := g_count + 1;
g_dept_ids(g_count) := v_dept_id;
END IF;
END AFTER EACH ROW;
AFTER STATEMENT IS
v_avg_salary NUMBER;
v_emp_count NUMBER;
BEGIN
-- ステートメント完了後に employees テーブルへのクエリが可能(ミュータル状態が解除)
FOR i IN 1..g_count LOOP
-- 重複を避けるため EXISTS チェックや DISTINCT 処理が必要な場合がある
SELECT AVG(salary), COUNT(*)
INTO v_avg_salary, v_emp_count
FROM employees
WHERE department_id = g_dept_ids(i);
-- 集計結果を別テーブルに格納する
MERGE INTO department_stats ds
USING DUAL ON (ds.department_id = g_dept_ids(i))
WHEN MATCHED THEN
UPDATE SET avg_salary = v_avg_salary,
emp_count = v_emp_count,
updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN
INSERT(department_id, avg_salary, emp_count, updated_at)
VALUES(g_dept_ids(i), v_avg_salary, v_emp_count, SYSTIMESTAMP);
END LOOP;
END AFTER STATEMENT;
END trg_dept_salary_stats;
/
大量 DML を一括処理する高速化パターン
行単位トリガーは処理する行数だけ起動されるため、大量 INSERT などでパフォーマンスが低下します。コンパウンドトリガーでは AFTER EACH ROW でデータを収集し、AFTER STATEMENT で FORALL を使って一括 INSERT することで高速化できます。
AFTER EACH ROW で収集して AFTER STATEMENT で FORALL 一括 INSERT
CREATE OR REPLACE TRIGGER trg_orders_audit_bulk
FOR INSERT ON orders
COMPOUND TRIGGER
-- 監査ログデータを一時保存するコレクション
TYPE t_order_ids IS TABLE OF orders.order_id%TYPE INDEX BY PLS_INTEGER;
TYPE t_cust_ids IS TABLE OF orders.customer_id%TYPE INDEX BY PLS_INTEGER;
TYPE t_amounts IS TABLE OF orders.total_amount%TYPE INDEX BY PLS_INTEGER;
g_order_ids t_order_ids;
g_cust_ids t_cust_ids;
g_amounts t_amounts;
g_count PLS_INTEGER := 0;
BEFORE STATEMENT IS
BEGIN
g_order_ids.DELETE;
g_cust_ids.DELETE;
g_amounts.DELETE;
g_count := 0;
END BEFORE STATEMENT;
AFTER EACH ROW IS
BEGIN
-- 各行のデータをコレクションに追加する
g_count := g_count + 1;
g_order_ids(g_count) := :NEW.order_id;
g_cust_ids(g_count) := :NEW.customer_id;
g_amounts(g_count) := :NEW.total_amount;
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
-- FORALL で一括 INSERT する(行レベルより大幅に高速)
FORALL i IN 1..g_count
INSERT INTO order_audit_log(order_id, customer_id, amount, inserted_at)
VALUES(g_order_ids(i), g_cust_ids(i), g_amounts(i), SYSTIMESTAMP);
END AFTER STATEMENT;
END trg_orders_audit_bulk;
/
まとめ
- COMPOUND TRIGGER:BEFORE STATEMENT / BEFORE EACH ROW / AFTER EACH ROW / AFTER STATEMENT を 1 つのトリガーに定義できる(Oracle 11g 以降)
- 共有変数:タイミングポイント間で変数を共有できる。BEFORE STATEMENT で初期化して AFTER STATEMENT で後処理するパターンが基本
- ORA-04091 解決:AFTER EACH ROW でキーを収集 → AFTER STATEMENT でミュータル状態解除後にクエリ。PRAGMA AUTONOMOUS_TRANSACTION による回避より正しい解法
- FORALL パターン:行レベルで個別 INSERT するより FORALL 一括 INSERT の方が大幅に高速。大量 DML に対するトリガーのパフォーマンス改善に有効
- 変数の初期化:BEFORE STATEMENT で必ず共有変数を初期化する。しないと前の DML の残データが混入する
ORA-04091 の詳細については ミュータルテーブルエラー完全ガイドを参照してください。FORALL / BULK COLLECT によるパフォーマンス改善は PL/SQL コレクション完全ガイドも参照してください。