トランジションテーブルは、PostgreSQL 10で導入された機能で、ステートメントレベルのトリガーのREFERENCING句を通じてこの機能を提供します。このメカニズムは、変更の全結果セットを一時テーブル構造として公開します。INSERT/UPDATE操作に対してはNEW TABLE、DELETE/UPDATE操作に対してはOLD TABLEを使用し、トリガー関数がセットベースの処理を実行できるようにします。行を個別に反復処理する代わりに、影響を受けたすべての行に同時に作用する単一のSQLステートメントを実行できます。
CREATE TRIGGER bulk_audit_trigger AFTER UPDATE ON inventory REFERENCING NEW TABLE AS updated_items OLD TABLE AS previous_items FOR EACH STATEMENT EXECUTE FUNCTION log_inventory_changes();
log_inventory_changes()内部で、updated_itemsをクエリすることでトリガーによって触れられたすべての行を取得でき、INSERT INTO audit_log SELECT * FROM updated_items;のような効率的なバルク操作が可能になります。
あるeコマースプラットフォームは、10万件の製品レコードに影響を与える夜間のバルク価格調整中に深刻なパフォーマンス低下に見舞われました。既存のアーキテクチャは行レベルのAFTERトリガーを使用して監査エントリを書き込み、100,000件のシーケンシャルなINSERTをprice_historyテーブルに実行して接続プールリソースを消耗させました。
解決策1: アプリケーション側のバッチ処理 チームはトリガーを削除し、Javaアプリケーション内でJDBCバッチ挿入を使用して監査を処理することを検討しました。この方法ではデータベースのCPU負荷を軽減できますが、重要な整合性リスクを引き起こしました:アプリケーションがバッチの途中でクラッシュした場合、コミットされた価格更新は永続的に対応する監査記録が欠如したままとなり、SOXコンプライアンスに違反しました。さらに、このアプローチはアプリサーバーとPostgreSQL間での複雑な分散トランザクション管理を必要としました。
解決策2: 非同期メッセージキュー 別の提案は、更新中に行識別子をRedisストリームに書き込み、その後バックグラウンドワーカーを介して監査を処理することでした。これにより書き込みパスが切り離されましたが、即時のトランザクションの整合性が犠牲となりました。非同期ワーカーは高負荷の際に遅延する可能性があり、規制監査官が指摘する一時的な監査トレイルのギャップを生じました。さらに、PostgreSQLとRedis間での一度だけのセマンティクスを確保するのは、大きなインフラストラクチャの複雑性を追加しました。
解決策3: トランジションテーブルを使用したステートメントレベルのトリガー
選択されたアプローチは、行トリガーをステートメントレベルのトリガーに置き換え、REFERENCING NEW TABLE AS new_pricesを利用しました。トリガー関数は単一のセットベースの操作を実行しました:INSERT INTO price_history SELECT product_id, old_price, new_price, NOW() FROM new_prices;。これにより、同じトランザクション内で厳格なACIDコンプライアンスが維持され、すべての変更を一度で処理できました。
結果: 夜間バッチの完了時間が45秒から300ミリ秒に短縮されました。WAL(Write-Ahead Log)生成が90%低下し、システムは以前に大量トリガーの再帰によって引き起こされたロック競合のスパイクを排除しました。
トランジションテーブルはBEFOREトリガーおよびビューに対するINSTEAD OFトリガーとどのように相互作用しますか?
トランジションテーブルは、普通のテーブルのAFTERトリガーでのみ利用可能です。BEFOREトリガーはステートメントの最終結果セットが具現化される前の個々の行に対して動作するため、変更された行の完全なコレクションはまだ存在しません。ビューに対するINSTEAD OFトリガーは、実際のDML操作の結果を観測するのではなく、代替の実行パスを定義するため、トランジションテーブルを使用できません。
トリガー関数は最終結果を変更するためにNEW TABLEまたはOLD TABLE内のデータを変更できますか?
いいえ、トランジションテーブルはトリガー実行中にのみアクセス可能な読み取り専用スナップショットです。これらはステートメントによって影響を受けた行の不変のビューを表し、UPDATE、DELETE、またはINSERT操作を使用して変更することはできません。コミットされる前に値を変更するには、NEWレコード変数を直接操作する行レベルのBEFOREトリガーを使用するか、永続的なテーブルに対して別のDMLを実行する必要があります。
なぜTRUNCATE操作はOLD TABLEトランジションテーブルをpopulateしないのですか?
PostgreSQLはTRUNCATEに関してステートメントレベルのトリガーをサポートしていますが、このDDLコマンドは個々の行のトランジション状態を生成することなくすべての行を削除します。TRUNCATEはトランザクションの関係を構築するために使用されるタプルバージョニングメカニズムをバイパスするため、削除された行でOLD TABLEをpopulateすることはできません。TRUNCATE操作の監査には、イベントトリガーまたは論理デコーディングストリームなどの代替メカニズムが必要です。