PostgreSQL: PostgreSQLのトリガーとイベントトリガー

最終更新:2026-08-26

1. 学習目標


2. ストーリー

AliceはECプラットフォームのデータベースアーキテクトです。2つのコアな自動化ロジックを実装する必要があります。

  1. 自動在庫減算: order_itemsに新規注文が挿入された際、BEFORE INSERTトリガーが自動的にproducts.stock_qtyを減算し、在庫不足の場合は挿入を拒否します。
  2. 価格監査ログ: 商品価格が更新された際、AFTER UPDATEトリガーが新旧の価格をprice_audit_logテーブルに記録します。

Aliceはこれらをトリガーで実装することを選択し、どのアプリケーションがデータを書き込んでもビジネスルールが一貫して適用されるようにします。


3. 概念: トリガーの基本

(1) トリガー種別の概要

タイミング 行レベル(FOR EACH ROW) 文レベル(FOR EACH STATEMENT)
BEFORE NEWを変更可能、操作を拒否可能 NEW/OLDなし、検証/準備可能
AFTER NEW/OLDを読み取り可能、ログ記録 集計統計に適する
INSTEAD OF ビューのみ、元の操作を置換 文レベルには非対応

▶ サンプル: 基本的なBEFORE INSERT行レベルトリガーの作成

SQL
CREATE OR REPLACE FUNCTION fn_before_order_item()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
  NEW.created_at := NOW();
  NEW.line_total := NEW.quantity * NEW.unit_price;
  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_before_order_item
  BEFORE INSERT ON order_items
  FOR EACH ROW
  EXECUTE FUNCTION fn_before_order_item();

Output:

TEXT 📖 参照専用
INSERT 0 1

▶ サンプル: AFTER UPDATE監査ログトリガー

SQL
CREATE OR REPLACE FUNCTION fn_audit_price_change()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
  IF NEW.unit_price IS DISTINCT FROM OLD.unit_price THEN
    INSERT INTO price_audit_log
      (product_id, old_price, new_price, changed_by, changed_at)
    VALUES
      (NEW.product_id, OLD.unit_price, NEW.unit_price,
       CURRENT_USER, NOW());
  END IF;
  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_audit_price
  AFTER UPDATE OF unit_price ON products
  FOR EACH ROW
  EXECUTE FUNCTION fn_audit_price_change();

Output:

TEXT 📖 参照専用
INSERT 0 1

(2) NEWとOLD変数

タイミング NEW OLD 変更可能
BEFORE INSERT あり(挿入される行) なし NEWを変更可能
BEFORE UPDATE あり(新しい値) あり(古い値) NEWを変更可能
BEFORE DELETE なし あり(削除される行) 変更不可
AFTER INSERT あり(読み取り専用) なし 変更不可
AFTER UPDATE あり(読み取り専用) あり(読み取り専用) 変更不可

▶ サンプル: BEFORE UPDATEでNEWを変更

SQL
CREATE OR REPLACE FUNCTION fn_auto_update_timestamp()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
  NEW.updated_at := NOW();
  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_products_updated_at
  BEFORE UPDATE ON products
  FOR EACH ROW
  EXECUTE FUNCTION fn_auto_update_timestamp();

Output:

TEXT 📖 参照専用
CREATE TABLE

4. 概念: 行レベルトリガーと文レベルトリガー

(1) 実行頻度の違い

観点 FOR EACH ROW FOR EACH STATEMENT
実行回数 影響を受ける行ごとに1回 SQL文ごとに1回
NEW/OLD 利用可能 利用不可
パフォーマンス影響 影響行数に比例 固定オーバーヘッド
典型的な用途 検証、カラム計算、カスケード 集計統計、キャッシュ更新

▶ サンプル: 文レベルトリガーで集計を更新

SQL
CREATE OR REPLACE FUNCTION fn_refresh_order_stats()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
  REFRESH MATERIALIZED VIEW CONCURRENTLY mv_order_stats;
  RETURN NULL;
END;
$$;

CREATE TRIGGER trg_refresh_order_stats
  AFTER INSERT OR UPDATE OR DELETE ON orders
  FOR EACH STATEMENT
  EXECUTE FUNCTION fn_refresh_order_stats();

Output:

TEXT 📖 参照専用
INSERT 0 1

▶ サンプル: 行レベルトリガーで在庫検証

SQL
CREATE OR REPLACE FUNCTION fn_check_stock()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
  v_stock INT;
BEGIN
  SELECT stock_qty INTO v_stock
  FROM products
  WHERE product_id = NEW.product_id;

  IF v_stock < NEW.quantity THEN
    RAISE EXCEPTION '在庫不足: 商品 % の在庫は % 個ですが、% 個要求されました',
      NEW.product_id, v_stock, NEW.quantity;
  END IF;

  UPDATE products
  SET stock_qty = stock_qty - NEW.quantity
  WHERE product_id = NEW.product_id;

  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_check_stock
  BEFORE INSERT ON order_items
  FOR EACH ROW
  EXECUTE FUNCTION fn_check_stock();

Output:

TEXT 📖 参照専用
INSERT 0 1

5. 概念: 条件付きトリガーと実行順序

(1) WHEN句

WHEN句は条件が満たされた場合のみトリガーを実行させ、不��な呼び出しオーバーヘッドを削減します。

▶ サンプル: WHEN条件付きトリガー

SQL
CREATE OR REPLACE FUNCTION fn_log_big_order()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
  INSERT INTO big_order_log (order_id, total_amount, created_at)
  VALUES (NEW.order_id, NEW.total_amount, NOW());
  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_log_big_order
  AFTER INSERT ON orders
  FOR EACH ROW
  WHEN (NEW.total_amount >= 5000)
  EXECUTE FUNCTION fn_log_big_order();

Output:

TEXT 📖 参照専用
INSERT 0 1

(2) トリガー実行順序のルール

ルール 説明
BEFOREがAFTERより先 すべてのBEFOREがすべてのAFTERより先に実行
同じタイミング、名前のアルファベット順 trg_atrg_bより先に実行
INSTEAD OFが元の操作を置換 ビュー▶み、元のINSERT/UPDATE/DELETEを実行しない
BEFOREがNULLを返すと操作をブロック UPDATE/INSERTでNULLを返すとその行をスキップ

▶ サンプル: 複数トリガーの実行順序

SQL
-- これらのトリガーはアルファベット順に実行: a -> b -> c
CREATE TRIGGER trg_a_validate
  BEFORE INSERT ON orders FOR EACH ROW
  EXECUTE FUNCTION fn_validate_order();

CREATE TRIGGER trg_b_calc_tax
  BEFORE INSERT ON orders FOR EACH ROW
  EXECUTE FUNCTION fn_calc_order_tax();

CREATE TRIGGER trg_c_notify
  AFTER INSERT ON orders FOR EACH ROW
  EXECUTE FUNCTION fn_notify_new_order();

Output:

TEXT 📖 参照専用
INSERT 0 1

6. 概念: INSTEAD OFトリガー

(1) ビュー上のINSTEAD OF

ビュー自体は直接のINSERT/UPDATE/DELETEをサポートしません。INSTEAD OFトリガーが操作をインターセプトし、実行ロジックをカスタマイズします。

プロパティ 説明
ビューのみ テーブルにINSTEAD OFは使用不可
FOR EACH ROW必須 文レベルは非サポート
元の操作を置換 デフォルトの動作は実行されない
更新可能ビューに適する ビューの書き込みをベーステーブルにマッピング

▶ サンプル: ビューへのINSTEAD OF INSERT

SQL
CREATE VIEW vw_customer_orders AS
SELECT
  c.customer_id,
  c.first_name,
  c.last_name,
  o.order_id,
  o.total_amount
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id;

CREATE OR REPLACE FUNCTION fn_insert_customer_order()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
  INSERT INTO customers (first_name, last_name)
  VALUES (NEW.first_name, NEW.last_name)
  ON CONFLICT DO NOTHING;

  INSERT INTO orders (customer_id, total_amount, order_date)
  SELECT customer_id, NEW.total_amount, CURRENT_DATE
  FROM customers
  WHERE first_name = NEW.first_name AND last_name = NEW.last_name;

  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_insert_customer_order
  INSTEAD OF INSERT ON vw_customer_orders
  FOR EACH ROW
  EXECUTE FUNCTION fn_insert_customer_order();

Output:

TEXT 📖 参照専用
INSERT 0 1

▶ サンプル: ビューへのINSTEAD OF UPDATE

SQL
CREATE OR REPLACE FUNCTION fn_update_customer_order()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
  UPDATE customers
  SET first_name = NEW.first_name,
      last_name = NEW.last_name
  WHERE customer_id = NEW.customer_id;

  UPDATE orders
  SET total_amount = NEW.total_amount
  WHERE order_id = NEW.order_id;

  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_update_customer_order
  INSTEAD OF UPDATE ON vw_customer_orders
  FOR EACH ROW
  EXECUTE FUNCTION fn_update_customer_order();

Output:

TEXT 📖 参照専用
CREATE TABLE

7. 概念: イベントトリガー

(1) DDLイベントトリガー(PGの特長)

イベントトリガーはDDLコマンド(CREATE/ALTER/DROP)で発火し、特定のテーブルに依存しません。

イベント タイミング
ddl_command_start DDL実行前
ddl_command_end DDL実行後
sql_drop DROPコマンド実行前
table_rewrite テーブル書き換え前(例: ALTER TYPE)

▶ サンプル: DROP TABLEを禁止するイベント��リガー

SQL
CREATE OR REPLACE FUNCTION fn_block_drop_table()
RETURNS EVENT_TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
  RAISE EXCEPTION '本番環境ではDROP TABLEは許可されていません!';
END;
$$;

CREATE EVENT TRIGGER etg_block_drop
  ON sql_drop
  WHEN tag IN ('DROP TABLE')
  EXECUTE FUNCTION fn_block_drop_table();

Output:

TEXT 📖 参照専用
CREATE TABLE

▶ サンプル: DDL監査ログ

SQL
CREATE TABLE ddl_audit_log (
  id SERIAL PRIMARY KEY,
  event_type TEXT,
  tag TEXT,
  object_type TEXT,
  object_name TEXT,
  command_text TEXT,
  current_user TEXT,
  event_time TIMESTAMP DEFAULT NOW()
);

CREATE OR REPLACE FUNCTION fn_log_ddl()
RETURNS EVENT_TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
  v_obj RECORD;
BEGIN
  v_obj := NULL;
  INSERT INTO ddl_audit_log (event_type, tag, object_type, object_name, current_user)
  VALUES (TG_EVENT, TG_TAG,
          v_obj.object_type, v_obj.object_identity,
          CURRENT_USER);
  RAISE NOTICE 'DDLをログ記録しました: % %', TG_EVENT, TG_TAG;
END;
$$;

CREATE EVENT TRIGGER etg_log_ddl
  ON ddl_command_end
  EXECUTE FUNCTION fn_log_ddl();

Output:

TEXT 📖 参照専用
INSERT 0 1

(2) イベントトリガーとテーブルトリガーの比較

観点 テーブルトリガー イベントトリガー
紐付くオブジェクト 特定のテーブル/ビュー グローバルなDDLイベント
DML/DDL DML(INSERT/UPDATE/DELETE) DDL(CREATE/ALTER/DROP)
NEW/OLD 利用可能 なし
TG_TAG なし あり(DROP TABLEなどのラベル)
典型的な用途 検証、監査、カスケード DDL監査、セキュリティ制御

8. 概念: 有効化/無効化とトリガーとアプリケーションロジックの比較

(1) トリガーの有効化/無効化

コマンド 効果
ALTER TABLE t DISABLE TRIGGER trg_name; 特定のトリガーを無効化
ALTER TABLE t ENABLE TRIGGER trg_name; 特定のトリガーを有効化
ALTER TABLE t DISABLE TRIGGER ALL; 全トリガーを無効化
ALTER TABLE t ENABLE TRIGGER ALL; 全トリガーを有効化

▶ サンプル: 一括インポート時にトリガーを無効化

SQL
-- 一括インポートのパフォーマンス向上のためトリガーを無効化
ALTER TABLE products DISABLE TRIGGER ALL;

COPY products(product_name, unit_price, category, stock_qty)
FROM '/data/products_bulk.csv' WITH (FORMAT csv, HEADER true);

-- インポート後に再有効化
ALTER TABLE products ENABLE TRIGGER ALL;

Output:

TEXT 📖 参照専用
-- SQL statement executed successfully

(2) トリガーとアプリケーションロジックの比較

観点 トリガー アプリケーションロジック
一貫性 あらゆる書き込み経路で発火 アプリのルール遵守に依存
デバッグ難易度 暗黙的、追跡困難 明示的呼び出し、デバッグ容易
パフォーマンス 行ごとに追加オーバーヘッド バッチ処理/最適化可能
移植性 PG固有の構文 汎用言語
ユースケース 不変条件の強制、監査、カスケード 複雑なフロー、クロスシステム

▶ サンプル: トリガーでデータ一貫性を強制

SQL
-- 強制: 注文合計は常に行項目の合計と等しいこと
CREATE OR REPLACE FUNCTION fn_enforce_order_total()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
  v_calculated NUMERIC;
BEGIN
  SELECT COALESCE(SUM(line_total), 0) INTO v_calculated
  FROM order_items
  WHERE order_id = NEW.order_id;

  IF v_calculated IS DISTINCT FROM (
    SELECT total_amount FROM orders WHERE order_id = NEW.order_id
  ) THEN
    UPDATE orders SET total_amount = v_calculated
    WHERE order_id = NEW.order_id;
  END IF;

  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_enforce_order_total
  AFTER INSERT OR UPDATE ON order_items
  FOR EACH ROW
  EXECUTE FUNCTION fn_enforce_order_total();

Output:

TEXT 📖 参照専用
  result  
----------
   42.50
(1 row)

9. フローチャート: トリガー選択の判断

100%
flowchart TD
    A[自動化ロジックが必要?] --> B{DMLかDDLか?}
    B -->|DDL| C[イベントトリガー<br/>EVENT TRIGGER]
    B -->|DML| D{テーブルかビューか?}
    D -->|ビュー| E[INSTEAD OFトリガー<br/>FOR EACH ROW]
    D -->|テーブル| F{操作前にデータを変更?}
    F -->|はい| G[BEFOREトリガー<br/>NEWを変更可能]
    F -->|いいえ| H{ログ/カスケードが必要?}
    H -->|はい| I[AFTERトリガー<br/>NEW/OLDを読み取り可能]
    H -->|いいえ| J[トリガー不要]
    G --> K{行ごとかSQLごとか?}
    I --> K
    K -->|行ごと| L[FOR EACH ROW]
    K -->|SQLごと| M[FOR EACH STATEMENT]
    L --> N{条件が必要?}
    N -->|はい| O[WHEN句]
    N -->|いいえ| P[条件なし]

    style C fill:#fff9c4
    style E fill:#e1bee7
    style G fill:#c8e6c9
    style I fill:#bbdefb

10. 総合サンプル

AliceのECトリガー構��——在庫減算+価格監査+タイムスタンプ自動更新。

SQL
-- 1. 商品変更時にタイムスタンプを自動更新
CREATE OR REPLACE FUNCTION fn_products_timestamp()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
  NEW.updated_at := NOW();
  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_products_timestamp
  BEFORE UPDATE ON products
  FOR EACH ROW
  EXECUTE FUNCTION fn_products_timestamp();

-- 2. 注文明細挿入時に在庫を減算、不足時は拒否
CREATE OR REPLACE FUNCTION fn_decrease_stock()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
  v_stock INT;
BEGIN
  SELECT stock_qty INTO v_stock FROM products
  WHERE product_id = NEW.product_id FOR UPDATE;

  IF v_stock IS NULL THEN
    RAISE EXCEPTION '商品 % が見つかりません', NEW.product_id;
  ELSIF v_stock < NEW.quantity THEN
    RAISE EXCEPTION '在庫不足: 商品 %(在庫=%, 要求=%)',
      NEW.product_id, v_stock, NEW.quantity;
  END IF;

  UPDATE products SET stock_qty = stock_qty - NEW.quantity
  WHERE product_id = NEW.product_id;
  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_decrease_stock
  BEFORE INSERT ON order_items
  FOR EACH ROW
  EXECUTE FUNCTION fn_decrease_stock();

-- 3. 価格変更時に監査ログを記録
CREATE OR REPLACE FUNCTION fn_price_audit()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
  IF NEW.unit_price IS DISTINCT FROM OLD.unit_price THEN
    INSERT INTO price_audit_log
      (product_id, old_price, new_price, changed_by, changed_at)
    VALUES
      (NEW.product_id, OLD.unit_price, NEW.unit_price,
       CURRENT_USER, NOW());
  END IF;
  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_price_audit
  AFTER UPDATE OF unit_price ON products
  FOR EACH ROW
  WHEN (OLD.unit_price IS DISTINCT FROM NEW.unit_price)
  EXECUTE FUNCTION fn_price_audit();

❓ よくある質問

Q BEFOREトリガーがNULLを返すとどうなりますか?
A INSERT/UPDATEの場合、NULLを返すとその行の操作がスキップされます(実際の書き込��は行われず、後続のトリガーも実行されません)。DELETEでは効果がありません。AFTERトリガーの戻り値は無視されることに注意してください。
Q テーブルに同じタイミングの複数トリガーがある場合、順序はどう決まりますか?
A PostgreSQLはトリガー名のアルファベット順に実行します。例: trg_atrg_bより先。すべてのBEFOREトリガーがすべてのAFTERトリガーより先に実行されます。
Q トリガーからCOMMITを実行できますか?
A 通常のテーブルトリガーはCOMMIT/ROLLBACKを実行できません(トランザクション内で動作します)。自律トランザクションにはトリガー関数内でdblinkを使用するか、PG 14+のプロシージャルアプローチを使用してください。
Q WHEN句の条件で他のテーブルを参照できますか?
A いいえ。WHEN条件はNEW/OLDカラムのみ参照できます。サブクエリや他テーブル参照は不可です。複雑な条件はトリガー関数本体内でチェックしてください。
Q INSTEAD OFトリガーでFOR EACH STATEMENTを使用できますか?
A いいえ。INSTEAD OFトリガーはFOR EACH ROWである必要があります。行ごとにベーステーブル操作へのマッピング方法を決定する必��があるためです。
Q イベントトリガーのTG_TAGとは何ですか?
A TG_TAGはイベントを発火させたDDLコマンドのラベルです。例: CREATE TABLEALTER TABLEDROP TABLE。WHEN tag IN (...)で絞り込み可能です。
Q トリガーを無効化するとCOPYインポートは速くなりますか?
A はい。大量データインポート時に行レベルトリガーを無効化するとパフォーマンスが大幅に向上しますが、再有効化後にトリガーが実行していたロジック(計算カラム、監査レコードなど)を手動で処理する必要があります。
Q 再帰的なトリガー呼び出しを回避するには?
A トリガーAがテーブルTを更新すると、再度Aが発火する可能性があります。WHEN条件、状態変数(パッケージレベル変数など)、またはAFTERトリガーに切り替えて条件付きチェックを行うことで回避してください。

📖 まとめ


📝 練習問題

  1. ⭐ BEFORE UPDATEトリガー関数とトリガーを作成��てください。customersテーブルのemailカラムが変更された際に、自動的にupdated_atNOW()に設定します。

  2. ⭐⭐ AFTER INSERTトリガーを作成してください。ordersテーブルにtotal_amount >= 1000 USDの新規注文が追加された際に、自動的にhigh_value_order_logテーブルに挿入します(order_id、total_amount、customer_id、created_atを記録)。

  3. ⭐⭐⭐ ビューvw_product_sales(products + order_itemsを結合して売上を計算)を作成し、INSTEAD OF UPDATEトリガーを作成して、ビュー内の変更されたtotal_soldproducts.stock_qtyの更新にマッピングしてください。さらに、すべてのCREATE TABLEDROP TABLEのDDL操作をddl_audit_logテーブルにログ記録するイベントトリガーを作成してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%