題幹與適用場景
PostgreSQL 18 允許 RETURNING 為 INSERT、UPDATE、DELETE 和 MERGE 明確返回舊行與新行。結果來自同一個變更語句,可以避免後續查詢與其他寫入者競爭。對於不存在的一側,舊值或新值可能為 NULL。
假設審計事件必須準確反映一個語句改變的值,重試需要冪等,應用不能承擔變更與審計之間的第二次讀取。
面試官考察點
面試官關注語句級原子性、不同操作的語義,以及交易邊界和重試方案。強回答會區分 INSERT、UPDATE、DELETE、ON CONFLICT 和 MERGE,並說明審計投遞與提交的關係。
普通回答寫入後再查詢一次。強回答把 RETURNING 當作變更輸出,保留操作身份,避免交易回滾後仍發布事件。
回答前需要釐清的問題
- 審計事件必須提交後發送,還是同交易寫 outbox 即可?
- 一個語句是否會修改多行?事件 ID 如何生成?
- upsert 衝突和每個 MERGE 分支中的「舊值」如何定義?
- 如何讓重試不產生重複審計?
- 哪些欄位必須在離開資料庫前去識別?
如果下游投遞必須跟隨提交,應在同一交易寫入 outbox,再非同步發布。如果審計只用於資料庫內部報表,呼叫方可以直接消費 RETURNING。
30 秒回答框架
「我會讓變更語句明確返回操作類型、舊值、新值和穩定事件鍵。多行寫入時,把這些結果在同一交易寫入 outbox,提交後再發布。插入和刪除要測試空值一側,明確 upsert 與 MERGE 語義,去識別敏感欄位,並用事件鍵保證重試冪等。」
分步驟深入解答
- 定義事件契約。 包含表識別、主鍵、操作、舊投影、新投影、交易或請求 ID 和冪等鍵。
- 使用明確別名。 使用文件支援的
RETURNING WITH (OLD AS oldrow, NEW AS newrow)語法,不依賴RETURNING *。 - 處理操作語義。 插入通常沒有舊行,刪除通常沒有新行;upsert 和 MERGE 需要按分支給出操作類型。
- 原子持久化。 在同一交易把返回記錄寫入 outbox。提交前的獨立查詢或外部發布可能看到最終回滾的資料。
- 保護資料與重試。 去識別欄位,必要時雜湊敏感值,並用唯一事件鍵避免重試重複投遞。
- 驗證並發。 測試並發寫入、衝突、回滾、多行語句和部分 MERGE 分支,把審計行與已提交表狀態比較。
替代方案包括集中執行的觸發器、全庫變更捕獲的邏輯解碼,或表達領域語義的應用事件。當變更語句已擁有精確行級結果時,RETURNING 最合適。
高品質示範回答
「應用更新返回明確操作、主鍵、舊投影、新投影和事件鍵。upsert 衝突標記為 update;MERGE 每個分支提供自己的操作。交易把所有返回行寫入 outbox 並一次提交,worker 提交後發布,用唯一事件鍵安全重試。刪除的新投影為空,插入的舊投影為空,敏感欄位在事件離開資料庫前移除。」
常見錯誤
- 錯誤表現: 變更後重新查詢 → 失敗原因: 其他寫入者可能已經改變行 → 修正方法: 消費同一語句的
RETURNING。 - 錯誤表現: 提交前發布 → 失敗原因: 審計事件可能描述回滾資料 → 修正方法: 使用交易 outbox。
- 錯誤表現: 把每次 upsert 都當作插入 → 失敗原因: 衝突更新有不同語義 → 修正方法: 明確返回操作類型。
- 錯誤表現: 無差別返回所有欄位 → 失敗原因: 密鑰或個人資料洩漏 → 修正方法: 使用允許列表投影和去識別。
追問及應對
INSERT 的 OLD 包含什麼?
通常沒有舊行,因此舊側為 NULL。事件契約應表達缺失,不應偽造預設行。
如何捕獲多行 MERGE?
每個受影響行消費一條 RETURNING 結果,包含分支操作,並在同一 outbox 交易寫入事件。
outbox 寫入失敗怎麼辦?
交易應失敗並回滾變更。不能確認業務寫入成功,卻靜默丟棄審計記錄。
什麼時候優先邏輯解碼?
需要全庫變更捕獲,或無法修改語句時使用邏輯解碼。需要領域投影和精確語句語義時優先 RETURNING。