トリガーという「劇薬」をどう飼いならすか ― PostgreSQLのトリガー関数を深く理解する
PostgreSQLを長年触っていると、若手からよく「トリガーって使ってもいいんですか?」という質問を受けます。答えはいつも同じ。「強力なツールだが、安易に使うとシステムが死ぬ」です。
トリガー関数は、単なる自動化ツールではありません。PostgreSQLのトランザクションライフサイクルに深く介入する、いわば「イベント駆動型のフック」です。今日は、教科書的な説明はすっ飛ばして、この「劇薬」の内部構造と、本番環境で地雷を踏まないための勘所について話しましょう。
1. トリガー関数の「特殊変数」が意味するもの
トリガー関数内で使える `NEW`、`OLD`、`TG_OP` といった特殊変数。これらは単なるレコードや文字列ではありません。
- `NEW` / `OLD` の正体: これらは、実行中の行レベル操作における「行のメモリ表現」です。注意すべきは、これらが `RECORD` 型であり、型チェックが実行時まで遅延されるという点です。頻繁に呼び出されるトリガーで `NEW.column_name` にアクセスする際、カラム名の間違いはコンパイル時には検知できません。大規模なテーブルでは、このメタデータ解決コストが意外とバカにならないのです。
- `TG_OP` と `TG_TABLE_NAME`: これらは、一つの汎用的な関数を複数のトリガーで使い回す際に非常に便利ですが、「分岐の数だけ性能コストを支払っている」という意識が必要です。特に `IF TG_OP = ‘INSERT’ THEN …` といった条件分岐が複雑になると、SQLの実行計画のキャッシュ効率に微細な影響を与えることもあります。
2. 内部アーキテクチャ:トリガーはどこで動いているのか
トリガー関数は、PostgreSQLの「エグゼキュータ(Executor)」の処理フローの中に割り込んで実行されます。
ここでの最大の落とし穴は、「トリガー関数が実行されている間、そのトランザクションは停止している」という事実です。もしトリガー内で重い計算や、外部APIへのアクセス(dblinkやpostgres_fdw経由)、あるいは競合の激しい別テーブルへの更新を走らせたらどうなるか。当然、メインのUPDATE/INSERTクエリのレイテンシが跳ね上がります。
特に「行レベルトリガー(FOR EACH ROW)」は要注意です。100万件のUPDATEを一括で行うクエリを発行したとき、トリガーが100万回起動されることを忘れてはいけません。MVCC(多版同時実行制御)のコンテキストで、この「100万回のコンテキストスイッチ」がどれほど重いか。パフォーマンスチューニングの現場では、トリガー内での処理をいかに「O(1)」に近づけるかが勝負になります。
3. トラブルシューティング:地雷を見抜くサイン
トリガーが原因のパフォーマンス劣化に遭遇したとき、私はまず `pg_stat_user_functions` を確認します。
SELECT funcname, calls, total_exec_time, self_exec_time
FROM pg_stat_user_functions
ORDER BY total_exec_time DESC;
ここで `total_exec_time` が異常に高いトリガー関数があれば、即座に犯人が特定できます。
また、よくある「はまりどころ」を挙げておきます。
- 無限ループの罠: `UPDATE` トリガーの中で、同じテーブルを `UPDATE` していないか? 再帰的な呼び出しを防ぐロジックがないと、スタックオーバーフローでプロセスがクラッシュします。
- トランザクション境界: トリガーは呼び出し元のトランザクションの一部です。トリガー内で `COMMIT` や `ROLLBACK` はできません(EXCEPTIONによる捕捉は可能ですが、扱いには細心の注意が必要です)。
- デッドロック: 複数のテーブルを更新するトリガーが絡み合うと、アプリケーション層からは全く見えない場所でデッドロックが発生します。トリガー内の更新順序は、常に固定しておくのが鉄則です。
4. 熟練者へのアドバイス:トリガーを「捨てる」勇気
ここまでトリガーの解説をしておいて何ですが、現代のアーキテクチャにおいて、「トリガーを極力使わない」という設計は非常に賢い選択です。
監査ログや複雑な計算は、アプリケーションコード側でハンドリングするか、いっそ非同期キュー(RabbitMQやKafkaなど)に流して別のプロセスで処理するほうが、PostgreSQLの負荷を低く保てます。
しかし、それでもトリガーを使うべき場面はあります。例えば、レガシーシステムとの共存や、物理層でどうしてもデータの整合性を担保しなければならない強固なビジネスルールがある場合です。そのときは、トリガーを「最後の砦」として使い、「トリガー内では重いロジックを書かない。書くならC言語やRustで書いた拡張機能で実行せよ」という設計思想を持つべきです。
PostgreSQLは、トリガーという強力な力を提供してくれます。その力をどう制御するかは、エンジニアである我々の腕次第です。皆さんのシステムが、今日も健やかにトランザクションをさばき続けていることを願っています。
コメント