PostgreSQL: PostgreSQLのトリガーとイベントトリガー
最終更新:2026-08-26
1. 学習目標
- CREATE TRIGGER(BEFORE / AFTER / INSTEAD OF)
- 行レベルトリガーと文レベルトリガー
- NEW / OLDレコード変数
- 条件付きトリガー(WHEN句)
- トリガー実行順序(名前のアルファベッ��順)
- INSTEAD OFトリガー(ビュー上)
- イベントトリガー(DDLイベント: CREATE/ALTER/DROP TABLE)
- トリガーの有効化/無効化
- トリガーとアプリケーションロジックの比較
2. ストーリー
AliceはECプラットフォームのデータベースアーキテクトです。2つのコアな自動化ロジックを実装する必要があります。
- 自動在庫減算:
order_itemsに新規注文が挿入された際、BEFORE INSERTトリガーが自動的にproducts.stock_qtyを減算し、在庫不足の場合は挿入を拒否します。 - 価格監査ログ: 商品価格が更新された際、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行レベルトリガーの作成
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:
INSERT 0 1
▶ サンプル: AFTER UPDATE監査ログトリガー
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:
INSERT 0 1
(2) NEWとOLD変数
| タイミング | NEW | OLD | 変更可能 |
|---|---|---|---|
| BEFORE INSERT | あり(挿入される行) | なし | NEWを変更可能 |
| BEFORE UPDATE | あり(新しい値) | あり(古い値) | NEWを変更可能 |
| BEFORE DELETE | なし | あり(削除される行) | 変更不可 |
| AFTER INSERT | あり(読み取り専用) | なし | 変更不可 |
| AFTER UPDATE | あり(読み取り専用) | あり(読み取り専用) | 変更不可 |
▶ サンプル: BEFORE UPDATEでNEWを変更
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:
CREATE TABLE
4. 概念: 行レベルトリガーと文レベルトリガー
(1) 実行頻度の違い
| 観点 | FOR EACH ROW | FOR EACH STATEMENT |
|---|---|---|
| 実行回数 | 影響を受ける行ごとに1回 | SQL文ごとに1回 |
| NEW/OLD | 利用可能 | 利用不可 |
| パフォーマンス影響 | 影響行数に比例 | 固定オーバーヘッド |
| 典型的な用途 | 検証、カラム計算、カスケード | 集計統計、キャッシュ更新 |
▶ サンプル: 文レベルトリガーで集計を更新
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:
INSERT 0 1
▶ サンプル: 行レベルトリガーで在庫検証
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:
INSERT 0 1
5. 概念: 条件付きトリガーと実行順序
(1) WHEN句
WHEN句は条件が満たされた場合のみトリガーを実行させ、不��な呼び出しオーバーヘッドを削減します。
▶ サンプル: WHEN条件付きトリガー
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:
INSERT 0 1
(2) トリガー実行順序のルール
| ルール | 説明 |
|---|---|
| BEFOREがAFTERより先 | すべてのBEFOREがすべてのAFTERより先に実行 |
| 同じタイミング、名前のアルファベット順 | trg_aがtrg_bより先に実行 |
| INSTEAD OFが元の操作を置換 | ビュー▶み、元のINSERT/UPDATE/DELETEを実行しない |
| BEFOREがNULLを返すと操作をブロック | UPDATE/INSERTでNULLを返すとその行をスキップ |
▶ サンプル: 複数トリガーの実行順序
-- これらのトリガーはアルファベット順に実行: 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:
INSERT 0 1
6. 概念: INSTEAD OFトリガー
(1) ビュー上のINSTEAD OF
ビュー自体は直接のINSERT/UPDATE/DELETEをサポートしません。INSTEAD OFトリガーが操作をインターセプトし、実行ロジックをカスタマイズします。
| プロパティ | 説明 |
|---|---|
| ビューのみ | テーブルにINSTEAD OFは使用不可 |
| FOR EACH ROW必須 | 文レベルは非サポート |
| 元の操作を置換 | デフォルトの動作は実行されない |
| 更新可能ビューに適する | ビューの書き込みをベーステーブルにマッピング |
▶ サンプル: ビューへのINSTEAD OF INSERT
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:
INSERT 0 1
▶ サンプル: ビューへのINSTEAD OF UPDATE
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:
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を禁止するイベント��リガー
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:
CREATE TABLE
▶ サンプル: DDL監査ログ
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:
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; |
全トリガーを有効化 |
▶ サンプル: 一括インポート時にトリガーを無効化
-- 一括インポートのパフォーマンス向上のためトリガーを無効化
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:
-- SQL statement executed successfully
(2) トリガーとアプリケーションロジックの比較
| 観点 | トリガー | アプリケーションロジック |
|---|---|---|
| 一貫性 | あらゆる書き込み経路で発火 | アプリのルール遵守に依存 |
| デバッグ難易度 | 暗黙的、追跡困難 | 明示的呼び出し、デバッグ容易 |
| パフォーマンス | 行ごとに追加オーバーヘッド | バッチ処理/最適化可能 |
| 移植性 | PG固有の構文 | 汎用言語 |
| ユースケース | 不変条件の強制、監査、カスケード | 複雑なフロー、クロスシステム |
▶ サンプル: トリガーでデータ一貫性を強制
-- 強制: 注文合計は常に行項目の合計と等しいこと
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:
result
----------
42.50
(1 row)
9. フローチャート: トリガー選択の判断
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トリガー構��——在庫減算+価格監査+タイムスタンプ自動更新。
-- 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();
❓ よくある質問
trg_aがtrg_bより先。すべてのBEFOREトリガーがすべてのAFTERトリガーより先に実行されます。CREATE TABLE、ALTER TABLE、DROP TABLE。WHEN tag IN (...)で絞り込み可能です。📖 まとめ
- タイミング: BEFORE(NEWを変更可能)、AFTER(ログ記録)、INSTEAD OF(ビューマッ��ング)
- 行レベルトリガーは行ごとに実行。文レベルはSQL文ごとに1回実行
- NEWは新しい値、OLDは古い値を保持。NEWはBEFOREフェーズで変更可能
- WHEN句で条件を絞り込み、不要なトリガー呼び出しを削減
- 同じタイミングのトリ��ーは名前のアルファベット順に実行
- INSTEAD OFはビューのみで、FOR EACH ROW必須
- イベントトリガー(PGの特長)はDDLコマンドをリッスン。監査とセキュリティ制御に適する
- DISABLE/ENABLE TRIGGERでトリガー状態を制御。一括インポート時に無効化で高速化
- トリガーはデータ一貫性を保証するがデバッグの複雑さが増す。慎重に検討すべき
📝 練習問題
-
⭐ BEFORE UPDATEトリガー関数とトリガーを作成��てください。
customersテーブルのemailカラムが変更された際に、自動的にupdated_atをNOW()に設定します。 -
⭐⭐ AFTER INSERTトリガーを作成してください。
ordersテーブルにtotal_amount >= 1000USDの新規注文が追加された際に、自動的にhigh_value_order_logテーブルに挿入します(order_id、total_amount、customer_id、created_atを記録)。 -
⭐⭐⭐ ビュー
vw_product_sales(products + order_itemsを結合して売上を計算)を作成し、INSTEAD OF UPDATEトリガーを作成して、ビュー内の変更されたtotal_soldをproducts.stock_qtyの更新にマッピングしてください。さらに、すべてのCREATE TABLEとDROP TABLEのDDL操作をddl_audit_logテーブルにログ記録するイベントトリガーを作成してください。