問題と背景
PostgreSQL 18 では、RETURNING を使用して、INSERT、UPDATE、DELETE、および MERGE の変更前および変更後の行の値を明示的に公開できます。結果はデータを変更する同じステートメントによって生成されるため、別のライターと競合する後続のクエリを回避できます。操作において該当する側が存在しない場合、変更前または変更後は NULL になる可能性があります。
監査イベントには 1 つのステートメントによって変更された正確な値が必要であり、リトライはべき等である必要があり、アプリケーションは変更と監査発行の間の 2 回目の読み取りを許容できないと想定します。
面接官が評価するポイント
面接官は、ステートメントレベルのアトミック性、正しい操作セマンティクス、ならびにトランザクション境界およびリトライの計画を評価します。優れた回答では、INSERT、UPDATE、DELETE、ON CONFLICT、および MERGE を区別し、コミットに対して監査配信がどこに位置づけられるかを説明します。
一般的な回答では、書き込み後に行を再度 SELECT します。優れた回答では、ミューテーションの出力として RETURNING を使用し、操作の同一性を保持し、トランザクションが後でロールバックするイベントを発行することを回避します。
最初に明確にすべき質問
- 監査イベントはコミット後にのみ発行する必要がありますか、それとも outbox 行で十分ですか?
- 1 つのステートメントで複数の行を変更できますか。また、イベント ID はどのように割り当てられますか?
- upsert の競合時、および各
MERGEアクションにおいて、「old」は何を意味しますか? - リトライ時に重複する監査イベントをどのように防ぎますか?
- データベースから出る前に秘匿化(マスキング)しなければならないカラムはありますか?
ダウンストリームへの配信をコミット後に実行する必要がある場合は、同じトランザクション内で outbox 行を挿入し、非同期で発行します。監査が内部 SQL レポート用のみである場合は、呼び出し元が RETURNING を直接消費できます。
30秒の回答
「ミューテーションステートメントが明示的な操作、変更前の値、変更後の値、および安定したイベントキーを返すようにします。複数行の書き込みの場合、同じトランザクション内でそれらの結果を outbox に挿入し、コミット後に発行します。insert および delete に対する NULL 側をテストし、upsert と MERGE のセマンティクスを定義し、機密カラムを秘匿化し、イベントキーを使用してリトライをべき等にします。」
ステップバイステップの設計
- イベント規約を定義する。 テーブル識別情報、プライマリキー、操作、古いプロジェクション、新しいプロジェクション、トランザクションまたはリクエスト ID、およびべき等性キーを含めます。
- 明示的なエイリアスを使用する。
RETURNING *に依存する代わりに、RETURNING WITH (OLD AS old_row, NEW AS new_row)または同等のドキュメント化された構文を記述します。 - 操作セマンティクスを処理する。 通常、insert には変更前の行がなく、delete には変更後の行がありません。upsert および
MERGEには、ブランチ固有の操作値が必要です。 - アトミックに永続化する。 返されたレコードを同じトランザクション内の outbox に挿入します。コミット前の個別読み取りや外部発行は、後でロールバックされる状態を観測してしまう可能性があります。
- データとリトライを保護する。 フィールドを秘匿化し、必要に応じて機密値をハッシュ化し、リトライされたステートメントによって重複配信されないよう一意のイベントキーを強制します。
- 並行性を検証する。 並行ライター、競合、ロールバック、複数行ステートメント、および部分的な
MERGEブランチを実行します。監査行とコミットされたテーブル状態を比較します。
代替案には、一元的な適用のためのトリガー、データベース全体のキャプチャのためのロジカルデコーディング、またはドメインセマンティクスのためのアプリケーションイベントが含まれます。RETURNING は、ミューテーション自体が正確な行レベルの変更をすでに保持している場合に最も強力です。
回答例
「アプリケーションの更新処理は、明示的な操作、プライマリキー、古いプロジェクション、新しいプロジェクション、およびイベントキーを返します。upsert の競合の場合、結果を update としてラベル付けし、MERGE の場合は各ブランチが独自の操作を提供します。トランザクションは返されたすべての行を outbox に挿入し、1 回コミットします。ワーカーはコミット後に一意のイベントキーを使用して発行し、安全にリトライします。delete は新しいプロジェクションが null になり、insert は古いプロジェクションが null になり、機密カラムはイベントがデータベースを離れる前に削除されます。」
よくある間違い
- 間違い: ミューテーション後に再クエリする → 失敗する理由: 別のライターが行を変更する可能性がある → 修正方法: 同じステートメントから
RETURNINGを消費する。 - 間違い: コミット前に発行する → 失敗する理由: 監査イベントがロールバックされたデータを記述する可能性がある → 修正方法: トランザクショナル outbox を使用する。
- 間違い: すべての upsert を insert として扱う → 失敗する理由: 競合による更新には異なるセマンティクスが必要である → 修正方法: 明示的な操作を発行する。
- 間違い: すべてのカラムを無差別に返す → 失敗する理由: シークレットや個人データが漏洩する → 修正方法: 許可リストのプロジェクションと秘匿化を使用する。
フォローアップの質問と回答
INSERT の場合、OLD には何が含まれますか?
通常、以前の行は存在しないため、古い側は NULL になります。イベント規約では、デフォルト行を創作するのではなく、その不在をモデル化する必要があります。
複数行の MERGE をどのようにキャプチャしますか?
影響を受ける行ごとに 1 つの RETURNING 結果を消費し、ブランチ操作を含め、同じ outbox トランザクションに各イベントを挿入します。
outbox への挿入が失敗した場合はどうなりますか?
トランザクションを失敗させ、ミューテーションをロールバックする必要があります。監査レコードを暗黙的に破棄しながら、ビジネス上の書き込みを承認してはいけません。
ロジカルデコーディングを選択するのはどのような場合ですか?
データベース全体の広範な変更キャプチャや、ステートメントを変更できないシステムにはロジカルデコーディングを使用します。アプリケーションがドメインを認識したプロジェクションや、ステートメントごとの正確なセマンティクスを必要とする場合は、RETURNING を優先します。