【Oracle】特定の列が更新されたときだけ発火するトリガーの作成方法|UPDATE OF・UPDATING()・NULL安全な変更検出・実務パターンまで解説

【Oracle】特定の列が更新されたときだけ発火するトリガーの作成方法|UPDATE OF・UPDATING()・NULL安全な変更検出・実務パターンまで解説 Oracle

Oracle のトリガーでは、テーブルへの UPDATE 全体ではなく特定の列が更新されたときだけ発火させることができます。給与が変更されたときだけ監査ログを記録する、ステータスが変わったときだけ通知を送る――こうした要件に最適です。

本記事では、UPDATE OF 句で列を指定する方法と、トリガー本体内で UPDATING('column') 述語を使って判定する方法の2 つのアプローチの違いを整理し、NULL を含む変更検出の罠複数列の指定、そして監査ログ・ステータス遷移チェック・変更禁止の実務パターンまで解説します。

この記事でわかること
・UPDATE OF column で特定列の更新時にだけトリガーを発火させる方法
・UPDATING(‘column’) 条件述語でトリガー本体内で列を判定する方法
・UPDATE OF と UPDATING() の違いと使い分け
・複数列の指定方法(OR / AND の組み合わせ)
・NULL を含む列の変更検出で :OLD <> :NEW が失敗する罠と対処法
・給与変更監査・ステータス遷移・変更禁止・変更履歴の実務パターン
スポンサーリンク

UPDATE OF 句で列を指定する

UPDATE OF column_name をトリガー定義に記述すると、その列が UPDATE の SET 句に含まれている場合のみトリガーが発火します。

SQL(UPDATE OF 基本)
-- salary 列が UPDATE されたときだけ発火
CREATE OR REPLACE TRIGGER trg_salary_audit
AFTER UPDATE OF salary ON employees
FOR EACH ROW
BEGIN
    INSERT INTO salary_audit
        (employee_id, old_salary, new_salary, changed_at, changed_by)
    VALUES
        (:OLD.employee_id, :OLD.salary, :NEW.salary, SYSDATE, USER);
END;
/

-- salary を更新 → トリガー発火
UPDATE employees SET salary = 5000 WHERE employee_id = 100;

-- department_id を更新 → トリガー発火しない
UPDATE employees SET department_id = 20 WHERE employee_id = 100;

複数列の指定(OR 条件)

SQL(複数列を UPDATE OF で指定)
-- salary または commission_pct が更新されたとき発火
CREATE OR REPLACE TRIGGER trg_compensation_audit
AFTER UPDATE OF salary, commission_pct ON employees
FOR EACH ROW
BEGIN
    INSERT INTO compensation_audit
        (employee_id, field_name, old_value, new_value, changed_at)
    VALUES
        (:OLD.employee_id,
         CASE WHEN UPDATING('SALARY') THEN 'SALARY'
              WHEN UPDATING('COMMISSION_PCT') THEN 'COMMISSION_PCT'
         END,
         CASE WHEN UPDATING('SALARY') THEN TO_CHAR(:OLD.salary)
              WHEN UPDATING('COMMISSION_PCT') THEN TO_CHAR(:OLD.commission_pct)
         END,
         CASE WHEN UPDATING('SALARY') THEN TO_CHAR(:NEW.salary)
              WHEN UPDATING('COMMISSION_PCT') THEN TO_CHAR(:NEW.commission_pct)
         END,
         SYSDATE);
END;
/
UPDATE OF はSET 句の「列名」で判定する
UPDATE OF salary は、SET 句に salary が含まれていれば発火します。値が実際に変わったかどうかは問いませんUPDATE employees SET salary = salary WHERE ... のように同じ値で更新しても発火します。「値が本当に変わったとき」だけ処理したい場合は、トリガー本体で :OLD.salary <> :NEW.salary を追加で判定してください。

UPDATING(‘column’) 条件述語

UPDATING('column_name') は、トリガー本体(BEGIN〜END 間)で使える条件述語です。UPDATE OF が「トリガー自体の発火条件」であるのに対し、UPDATING() は「発火後に本体内で列を判定」する機能です。

SQL(UPDATING() で列ごとに処理を分岐)
-- INSERT / UPDATE / DELETE に対応する汎用トリガー内で
-- 特定列の更新時だけ追加処理を行う
CREATE OR REPLACE TRIGGER trg_emp_all_changes
AFTER INSERT OR UPDATE OR DELETE ON employees
FOR EACH ROW
BEGIN
    -- 共通: 全 DML の監査ログ
    INSERT INTO audit_log (table_name, action, action_date)
    VALUES ('EMPLOYEES',
            CASE WHEN INSERTING THEN 'INSERT'
                 WHEN UPDATING  THEN 'UPDATE'
                 WHEN DELETING  THEN 'DELETE' END,
            SYSDATE);

    -- salary が更新されたときだけ追加処理
    IF UPDATING('SALARY') THEN
        INSERT INTO salary_audit (employee_id, old_sal, new_sal, changed_at)
        VALUES (:OLD.employee_id, :OLD.salary, :NEW.salary, SYSDATE);
    END IF;

    -- status が更新されたときだけ追加処理
    IF UPDATING('STATUS') THEN
        -- ステータス変更通知
        NULL;  -- 通知処理
    END IF;
END;
/

UPDATE OF と UPDATING() の違い

項目 UPDATE OF column UPDATING(‘column’)
記述場所 トリガー定義(CREATE TRIGGER の ON 句の前) トリガー本体(BEGIN〜END 間の IF 文)
効果 トリガー自体の発火を制御(発火しなければ本体は実行されない) 発火後に本体内で列ごとの処理を分岐
複数列の扱い UPDATE OF col1, col2(OR 条件: いずれかが SET にあれば発火) IF UPDATING(‘COL1’) AND UPDATING(‘COL2’)(AND/OR 自由)
INSERT / DELETE との併用 UPDATE のみ(INSERT OR UPDATE OF col は可) INSERT/UPDATE/DELETE 全対応のトリガー内で使える
パフォーマンス 不要な発火を防げる(効率的 毎回発火してから判定する
値の変更有無 判定しない(SET に列があれば発火) 判定しない(SET に列があれば TRUE)
使い分けの指針
特定列の更新時にだけトリガーを発火させたいUPDATE OF column(不要な発火を防ぎパフォーマンス向上)
1 つのトリガーで複数の DML を処理しつつ、列ごとに分岐したいUPDATING('column')
両方を組み合わせることもできます(UPDATE OF で発火を絞り、本体で更に分岐)

NULL を含む変更検出の罠と対処法

UPDATE OF も UPDATING() も「SET 句に列が含まれているか」を判定するだけで、値が実際に変わったかどうかは判定しません。値の変更を検出するには :OLD と :NEW を比較する必要がありますが、NULL が含まれると正しく動作しない罠があります。

SQL(NULL の罠)
-- NG: NULL を含む比較が正しく動かない
IF :OLD.commission_pct <> :NEW.commission_pct THEN
    -- :OLD が NULL または :NEW が NULL の場合、<> は UNKNOWN を返す
    -- → このブロックは実行されない!
END IF;

-- 例: commission_pct が NULL → 0.1 に変更されても検出されない
SQL(NULL 安全な変更検出)
-- 方法(1): NVL で NULL を代替値に変換
IF NVL(:OLD.commission_pct, -1) <> NVL(:NEW.commission_pct, -1) THEN
    -- NULL → 0.1 の変更も検出可能
END IF;

-- 方法(2): DECODE で NULL 同士を等値判定
IF DECODE(:OLD.commission_pct, :NEW.commission_pct, 0, 1) = 1 THEN
    -- DECODE は NULL 同士を「等しい」と扱う → 変更時のみ 1 を返す
END IF;

-- 方法(3): OR IS NULL パターン(汎用)
IF (:OLD.commission_pct <> :NEW.commission_pct)
   OR (:OLD.commission_pct IS NULL AND :NEW.commission_pct IS NOT NULL)
   OR (:OLD.commission_pct IS NOT NULL AND :NEW.commission_pct IS NULL) THEN
    -- 完全な変更検出(冗長だが最も確実)
END IF;
WHEN 句でも NULL 安全に書ける
トリガー定義の WHEN 句では NVL が使えます。
WHEN (NVL(NEW.salary, -1) <> NVL(OLD.salary, -1))
WHEN 句でフィルタすれば、値が変わっていない場合はトリガー本体が実行されず効率的です。注意: WHEN 句ではコロンなし(NEW / OLD)で記述します。

実務パターン集

パターン(1): 給与変更の監査ログ(値が本当に変わったときだけ)

SQL(給与変更監査)
CREATE OR REPLACE TRIGGER trg_salary_change_audit
AFTER UPDATE OF salary ON employees
FOR EACH ROW
WHEN (NVL(NEW.salary, -1) <> NVL(OLD.salary, -1))  -- 値が実際に変わった場合のみ
BEGIN
    INSERT INTO salary_audit
        (employee_id, old_salary, new_salary, change_pct,
         changed_at, changed_by)
    VALUES (
        :OLD.employee_id,
        :OLD.salary,
        :NEW.salary,
        ROUND((:NEW.salary - :OLD.salary) / NULLIF(:OLD.salary, 0) * 100, 2),
        SYSDATE,
        USER
    );
END;
/

パターン(2): ステータス遷移のバリデーション

SQL(不正なステータス遷移を禁止)
-- ステータスの遷移ルール:
-- PENDING → ACTIVE → INACTIVE(逆方向は禁止)
CREATE OR REPLACE TRIGGER trg_status_transition
BEFORE UPDATE OF status ON orders
FOR EACH ROW
BEGIN
    -- INACTIVE から ACTIVE への逆遷移を禁止
    IF :OLD.status = 'INACTIVE' AND :NEW.status = 'ACTIVE' THEN
        RAISE_APPLICATION_ERROR(-20010,
            'INACTIVE から ACTIVE への遷移は許可されていません');
    END IF;

    -- ACTIVE から PENDING への逆遷移を禁止
    IF :OLD.status = 'ACTIVE' AND :NEW.status = 'PENDING' THEN
        RAISE_APPLICATION_ERROR(-20011,
            'ACTIVE から PENDING への遷移は許可されていません');
    END IF;
END;
/

パターン(3): 特定列の変更を禁止する

SQL(列の変更禁止)
-- employee_id(主キー)の変更を禁止
CREATE OR REPLACE TRIGGER trg_prevent_id_change
BEFORE UPDATE OF employee_id ON employees
FOR EACH ROW
BEGIN
    RAISE_APPLICATION_ERROR(-20020,
        'employee_id の変更は禁止されています');
END;
/

-- 特定条件下でのみ変更を禁止(承認済みレコードの金額変更禁止)
CREATE OR REPLACE TRIGGER trg_prevent_amount_change
BEFORE UPDATE OF amount ON invoices
FOR EACH ROW
WHEN (OLD.status = 'APPROVED')
BEGIN
    IF :OLD.amount <> :NEW.amount THEN
        RAISE_APPLICATION_ERROR(-20021,
            '承認済みの請求書の金額は変更できません');
    END IF;
END;
/

パターン(4): 変更された列だけを履歴テーブルに記録

SQL(変更された列のみ記録)
CREATE OR REPLACE TRIGGER trg_emp_change_detail
AFTER UPDATE ON employees
FOR EACH ROW
DECLARE
    PROCEDURE log_change(p_col VARCHAR2, p_old VARCHAR2, p_new VARCHAR2) IS
    BEGIN
        IF NVL(p_old, CHR(0)) <> NVL(p_new, CHR(0)) THEN
            INSERT INTO change_history
                (table_name, record_id, column_name, old_value, new_value, changed_at)
            VALUES
                ('EMPLOYEES', :OLD.employee_id, p_col, p_old, p_new, SYSDATE);
        END IF;
    END;
BEGIN
    log_change('SALARY',         TO_CHAR(:OLD.salary),         TO_CHAR(:NEW.salary));
    log_change('LAST_NAME',      :OLD.last_name,               :NEW.last_name);
    log_change('DEPARTMENT_ID',  TO_CHAR(:OLD.department_id),  TO_CHAR(:NEW.department_id));
    log_change('COMMISSION_PCT', TO_CHAR(:OLD.commission_pct), TO_CHAR(:NEW.commission_pct));
END;
/

パターン(5): 変更通知(条件付き)

SQL(特定条件での通知)
-- 給与が 20% 以上変更された場合にアラートテーブルに記録
CREATE OR REPLACE TRIGGER trg_salary_alert
AFTER UPDATE OF salary ON employees
FOR EACH ROW
WHEN (NEW.salary > OLD.salary * 1.2 OR NEW.salary < OLD.salary * 0.8)
BEGIN
    INSERT INTO salary_alerts
        (employee_id, old_salary, new_salary, change_pct, alert_date)
    VALUES (
        :OLD.employee_id,
        :OLD.salary,
        :NEW.salary,
        ROUND((:NEW.salary - :OLD.salary) / :OLD.salary * 100, 1),
        SYSDATE
    );
END;
/

よくある質問

QUPDATE OF で指定した列以外が SET に含まれていてもトリガーは発火しますか?
AUPDATE OF salary と定義した場合、SET 句に salary が含まれていればトリガーは発火します。他の列(例: department_id)も同じ UPDATE 文に含まれていても問題ありません。逆に、salary が SET 句にない場合はトリガーは発火しません。
QUPDATE employees SET salary = salary でもトリガーは発火しますか?
Aはい。UPDATE OF salary は SET 句に salary が含まれているかどうかだけを見ます。値が実際に変わったかどうかは判定しません。値の変更を検出したい場合は WHEN 句で WHEN (NVL(NEW.salary, -1) <> NVL(OLD.salary, -1)) を追加してください。
QUPDATING() の引数は大文字で指定する必要がありますか?
Aはい。UPDATING('SALARY') のように大文字で指定してください。Oracle は列名を内部的に大文字で格納するため、UPDATING('salary')(小文字)だと正しく判定されません。ただし、ダブルクォートで作成した列名の場合はそのままの大文字小文字で指定します。
Q:OLD と :NEW の比較で NULL が含まれると検出できません
A:OLD.col <> :NEW.col は一方が NULL の場合に UNKNOWN を返し、IF 文の条件が FALSE 扱いになります。NVL(:OLD.col, 代替値) <> NVL(:NEW.col, 代替値) で NULL を代替値に変換するか、DECODE(:OLD.col, :NEW.col, 0, 1) = 1 を使ってください。
QWHEN 句と UPDATE OF は併用できますか?
Aはい。AFTER UPDATE OF salary ON employees FOR EACH ROW WHEN (NEW.salary > 10000) のように両方指定できます。UPDATE OF で「salary が SET に含まれる」かつ WHEN で「新しい salary が 10000 超」の場合のみトリガーが発火します。不要な発火を最大限抑えられるため、パフォーマンス面でも有効です。
QINSERT OR UPDATE OF salary と書けますか?
Aはい。AFTER INSERT OR UPDATE OF salary ON employees と書けます。INSERT 時は常に発火し、UPDATE 時は salary が SET に含まれる場合のみ発火します。DELETE と組み合わせる場合は AFTER INSERT OR UPDATE OF salary OR DELETE ON employees と書きます。

まとめ

特定列の更新トリガーの要点をまとめます。

やりたいこと 方法
salary が SET に含まれるときだけ発火 AFTER UPDATE OF salary ON table
salary または bonus が SET に含まれるとき発火 AFTER UPDATE OF salary, bonus ON table
トリガー本体内で列ごとに処理を分岐 IF UPDATING(‘SALARY’) THEN … END IF
値が実際に変わったときだけ処理 WHEN (NVL(NEW.col, -1) <> NVL(OLD.col, -1))
NULL を含む列の変更検出 NVL(:OLD.col, x) <> NVL(:NEW.col, x) または DECODE
特定列の変更を禁止 BEFORE UPDATE OF col … RAISE_APPLICATION_ERROR
変更された列だけ履歴テーブルに記録 :OLD と :NEW を比較して差分がある列のみ INSERT
大幅変更(20% 以上)のアラート WHEN (NEW.col > OLD.col * 1.2)

トリガーの基本構文は「トリガーを作成する方法」、複合トリガー・DDL トリガーなど高度なトピックは「トリガー高度活用完全ガイド」も併せて参照してください。