PostgreSQL | SQLite+MySQL経験者向け、30日で習得するPostgreSQL:プロレベル運用 - Day25 トリガー

SQL PostgreSQL
スポンサーリンク

Day25 前半のゴール

「“INSERTしたら勝手に何かしてくれる”を安全にイメージできるようになる」

今日のテーマはトリガーです。
一言でいうと「テーブルに対する操作(INSERT / UPDATE / DELETE)をきっかけに、自動で何かをさせる仕組み」です。

前半のゴールは次のイメージを持つことです。
トリガーとは何かを、自分の言葉で説明できる。
トリガーが「いつ動くか」「何を呼ぶか」の関係を理解する。
シンプルな「監査ログ用トリガー」の例で、動きの流れを追える。

ここから、少しずつ分解していきます。


トリガーとは何か

「テーブルに“フック”を仕込んでおくイメージ」

トリガー(trigger)は、テーブルに対して仕込んでおく「フック」のようなものです。
ある操作が行われたときに、自動的に決められた処理を呼び出します。

PostgreSQL のトリガーは、ざっくりこういう構造です。

どのテーブルに
どの操作(INSERT / UPDATE / DELETE / TRUNCATE)で
いつ(BEFORE / AFTER)
どの関数(トリガー関数)を呼ぶか

を定義します。

重要なのは、「トリガー自体は“設定”であり、実際の処理は“トリガー関数”に書く」という分離です。
トリガーは「きっかけ」、トリガー関数は「中身のロジック」と覚えておくと整理しやすいです。


トリガーが役立つ典型パターン

「“毎回同じことをやるなら、DBに覚えさせる”」

トリガーがよく使われる場面を、イメージしやすい言葉で挙げてみます。

監査ログ
あるテーブルが更新されたら、「誰が・いつ・何を変えたか」を別テーブルに自動記録する。

自動補完
INSERT の前に、足りない値(作成日時・更新日時など)を自動で埋める。

整合性維持
あるテーブルが変わったら、関連する集計テーブルやキャッシュテーブルを更新する。

アプリ側で毎回同じ処理を書くより、
「DBに一度ルールとして覚えさせる」ほうが安全で漏れがない、というタイプの処理に向いています。

ただし、やりすぎると「何がどこで起きているか分からないブラックボックスDB」になるので、設計が大事です。
Day25 では、「どこまでをトリガーに任せるか」という感覚も一緒に育てていきます。


トリガーの基本構造をざっくり掴む

「トリガー関数+CREATE TRIGGER の二段構え」

トリガーを使うには、必ず二つのステップがあります。

一つ目は「トリガー関数」を作ること。
これは PL/pgSQL で書く、特殊な形の関数です。
戻り値の型が trigger で、NEWOLD という特別なレコードを扱います。

二つ目は「CREATE TRIGGER」で、テーブルにトリガーを紐づけること。
どのタイミングで、どのトリガー関数を呼ぶかを設定します。

イメージとしては、

トリガー関数=「自動でやりたい処理の中身」
CREATE TRIGGER=「どのテーブルのどの操作をきっかけに、その処理を呼ぶか」

という役割分担です。


例題:更新履歴を残すシンプルな監査トリガー

「usersテーブルのUPDATEを、別テーブルに記録する」

具体例で流れを見てみましょう。
よくあるのが、「重要なテーブルの変更履歴を残したい」という要件です。

ここでは、users テーブルがあるとします。

CREATE TABLE users (
  id         bigserial PRIMARY KEY,
  name       text NOT NULL,
  email      text NOT NULL,
  updated_at timestamptz NOT NULL DEFAULT now()
);
SQL

この users が更新されたときに、「更新前後の値」と「更新時刻」を audit_users というテーブルに記録したいとします。

まず、監査用テーブルを作ります。

CREATE TABLE audit_users (
  id           bigserial PRIMARY KEY,
  user_id      bigint NOT NULL,
  old_name     text,
  old_email    text,
  new_name     text,
  new_email    text,
  changed_at   timestamptz NOT NULL DEFAULT now()
);
SQL

ここに、「変更前(old_〜)」と「変更後(new_〜)」を保存するイメージです。

次に、この audit_users に書き込むトリガー関数を作ります。


トリガー関数の中身を理解する

「OLDとNEWという“特別なレコード”」

トリガー関数は、普通の PL/pgSQL 関数と少しだけ違います。
典型的な形はこうです。

CREATE OR REPLACE FUNCTION trg_users_audit()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
  INSERT INTO audit_users (
    user_id,
    old_name,
    old_email,
    new_name,
    new_email,
    changed_at
  )
  VALUES (
    OLD.id,
    OLD.name,
    OLD.email,
    NEW.name,
    NEW.email,
    now()
  );

  RETURN NEW;
END;
$$;
SQL

ここで重要なポイントを深掘りします。

戻り値の型が RETURNS trigger になっていること。
これが「トリガー関数ですよ」という印です。

関数の引数はありません。
代わりに、関数の中で OLDNEW という特別なレコードが使えます。

UPDATE の場合、
OLD は「更新前の行」
NEW は「更新後の行」

を表します。

この関数では、

OLD から id, name, email を取り出して old_〜 に入れ、
NEW から name, email を取り出して new_〜 に入れ、
現在時刻を changed_at に入れて、audit_users に INSERT しています。

最後の RETURN NEW; は、「このトリガーの後にテーブルに反映される行」を返す、という意味です。
BEFORE トリガーの場合は、ここで NEW を書き換えることで、実際に保存される値を変えることもできます。
今回は AFTER トリガーを想定しているので、そのまま NEW を返しています。


CREATE TRIGGERで“いつ動かすか”を決める

「AFTER UPDATE ON users FOR EACH ROW」

トリガー関数ができたら、それをテーブルに紐づけます。

CREATE TRIGGER users_audit_trigger
AFTER UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION trg_users_audit();
SQL

この1文で、「トリガーとしての設定」が完了します。

AFTER UPDATE ON users
users テーブルに対する UPDATE が終わったあとで、このトリガーを動かす、という意味です。

FOR EACH ROW
UPDATE 文で影響を受けた「各行ごと」にトリガー関数を呼ぶ、という指定です。
例えば、1回の UPDATE で10行更新されたら、トリガー関数も10回呼ばれます。

EXECUTE FUNCTION trg_users_audit();
さっき作ったトリガー関数を呼びなさい、という指定です。

これで、「users が更新されるたびに、OLD / NEW を使って audit_users に履歴を残す」という自動処理が完成します。


実際に動くイメージを追ってみる

「UPDATE 1回で、裏側で何が起きているか」

ここまで来たら、頭の中で「1回の UPDATE で何が起きるか」をシミュレーションしてみましょう。

まず、アプリやコンソールから、普通に UPDATE を打ちます。

UPDATE users
   SET name = '新しい名前'
 WHERE id = 1;
SQL

PostgreSQL の中では、次のような流れになります。

users テーブルの id=1 の行を探す。
その行の「更新前の値」を OLD として保持する。
更新後の行(name が変わったもの)を NEW として用意する。
UPDATE の処理が終わったタイミングで、「AFTER UPDATE ON users」のトリガーを探す。
users_audit_trigger が見つかるので、trg_users_audit() を呼び出す。
trg_users_audit の中で、OLD / NEW を使って audit_users に INSERT する。
トリガー関数が NEW を返すので、そのまま処理を完了する。

結果として、

users テーブルには新しい name が反映される。
audit_users テーブルには、「変更前と変更後の値+時刻」が1行追加される。

呼び出し側(アプリやSQLクライアント)は、UPDATE を1回打っただけです。
裏側でトリガーが静かに動いて、履歴を残してくれています。


トリガーの“気持ちよさ”と“怖さ”

「便利さとブラックボックス化の境界線」

ここまでの例を見ると、「トリガーめちゃくちゃ便利じゃん」と感じると思います。
実際、監査ログや自動補完などにはとても相性がいいです。

ただし、トリガーには「気持ちよさ」と同じくらい「怖さ」もあります。

コードを読んでいるだけでは、「裏で何のトリガーが動いているか」が見えにくい。
1つの操作が、複数のトリガーを連鎖的に呼び出すと、挙動が追いづらくなる。
パフォーマンス問題が起きたとき、「トリガーの中身」がボトルネックになっていることもある。

だから、プロレベル運用では、

どのテーブルにどんなトリガーが付いているかをドキュメント化する。
トリガーの中身はできるだけシンプルに保つ。
重い処理や外部サービス連携は、トリガーではなく別の仕組みに逃がす。

といった「設計と運用のルール」を一緒に考えます。

Day25 前半では、「トリガーは“テーブル操作にフックを仕込む仕組み”であり、トリガー関数+CREATE TRIGGER の二段構えで設計する」という構造が分かっていれば十分です。


Day25 前半のまとめ

トリガーは「特定のテーブルに対する INSERT / UPDATE / DELETE / TRUNCATE をきっかけに、自動でトリガー関数を呼び出す仕組み」であり、実体のロジックは RETURNS trigger な PL/pgSQL 関数の中に書き、CREATE TRIGGER で「どのテーブルの、どの操作の、どのタイミング(BEFORE / AFTER)で、その関数を呼ぶか」を結びつける。
UPDATE 時に OLD(更新前)と NEW(更新後)の行を使って監査テーブルに履歴を残す例のように、「毎回同じことをやりたい処理(監査ログ・自動補完・整合性維持など)」をDB側に一度覚えさせることで、アプリ側の書き忘れを防げる一方、やりすぎると“何がどこで起きているか分からないブラックボックスDB”になるため、トリガーは構造と役割を意識して慎重に設計する――ここまでの感覚が持てれば、Day25 前半としてはとても良い状態です。

タイトルとURLをコピーしました