具代表性的面試主題

如何用 PostgreSQL 18 temporal constraints 防止時間區間重疊?

後端困難
Offer.cc 編輯團隊發佈 更新

題幹

請設計房間預訂表,要求同一房間的有效時間區間不能重疊,並讓預訂明細引用對應期間。說明 PostgreSQL 18 的約束語法、邊界語義、遷移和並行驗證。

題目與背景

系統保存房間預訂及其明細。過去由應用先查詢衝突再插入,競爭請求仍可能產生重疊區間。請用 PostgreSQL 18 的 temporal constraints 在資料庫層保證同一房間的期間不重疊,並讓子表引用覆蓋父表的有效期間。

面試官考察什麼

重點是理解 WITHOUT OVERLAPS 用於主鍵或唯一約束的最後一個範圍欄位,PERIOD 用於期間外鍵;資料庫會把相同前綴鍵的非空範圍視為不可重疊集合。答案還要說明半開區間、空範圍、NULL、更新拆分、現有髒資料遷移及並行衝突處理。

先問清楚的澄清問題

時間模型

確認使用 tstzrange 還是 daterange、時區和邊界是否採用半開區間。區間端點和相鄰預訂是否允許接觸會直接影響約束結果。

業務鍵與引用關係

確認房間 ID 是否構成業務前綴、明細期間必須完全落在父期間還是只需存在重疊,以及是否允許一筆預訂跨越多個版本。

遷移與並行

確認舊表是否已有重疊或空區間、遷移窗口和失敗回滾方式。並行插入必須依賴資料庫約束和交易錯誤處理,不能只依賴應用鎖。

30 秒回答框架

「我把房間 ID 作為前綴,把有效期範圍作為最後一欄,用 PRIMARY KEY (room_id, during WITHOUT OVERLAPS) 保證同房間期間不重疊。明細表用 FOREIGN KEY (room_id, PERIOD during) 引用父表的期間鍵。先清理重疊和空範圍,再分階段加約束;並行衝突讓交易捕獲唯一或排他性錯誤並重試,明確半開區間與時區規則。」

深入解答步驟

第一步:選擇範圍型別和邊界

使用適合業務的離散或連續範圍型別,並統一半開區間約定。拒絕空範圍,明確相鄰區間是否可接受;時區時間必須統一轉換,避免夏令時造成意外重疊。

第二步:定義 temporal 主鍵

把實體識別欄位放在前面,範圍欄位放在最後,並使用 WITHOUT OVERLAPS。該約束表達同一實體前綴下的範圍不可重疊,同時繼續提供主鍵的非空和唯一身份語義。

sql
CREATE TABLE room_booking (
  room_id bigint NOT NULL,
  during tstzrange NOT NULL,
  guest_id bigint NOT NULL,
  PRIMARY KEY (room_id, during WITHOUT OVERLAPS)
);

第三步:定義期間外鍵

如果明細記錄也帶有期間,使用 PERIOD 引用父表的 temporal 主鍵或唯一約束。確認業務需要的是完整覆蓋語義,並測試父記錄拆分或縮短時的級聯行為。

sql
CREATE TABLE booking_charge (
  room_id bigint NOT NULL,
  during tstzrange NOT NULL,
  amount numeric NOT NULL,
  FOREIGN KEY (room_id, PERIOD during)
    REFERENCES room_booking (room_id, PERIOD during)
);

第四步:清理歷史資料

上線前找出同一房間的重疊、空範圍、NULL 和非法邊界,制定合併、拆分或作廢策略。先在影子表驗證約束,再分批修復,避免一次遷移鎖住大表。

第五步:處理並行寫入

兩個交易同時插入相同房間和重疊期間時,讓資料庫判定衝突;應用捕獲約束錯誤後重新讀取可用時間或返回明確衝突。不要把先查後插當作唯一保護,也不要用不可控的全域鎖替代約束。

第六步:評估更新和刪除語義

更新範圍可能與自身或其他資料列衝突,拆分預訂應在一個交易中完成。刪除或縮短父期間前,驗證期間外鍵的動作規則,避免產生孤兒明細或隱式擴大父期間。

第七步:驗證查詢與維運

測試相鄰、包含、完全相同、空範圍、跨時區和邊界精度場景。監控約束錯誤率、遷移鎖等待和索引大小;為寫入 API 設計重試上限,防止高競爭下形成重試風暴。

高品質示例回答

我會選擇 tstzrange 並統一半開區間,把 (room_id, during WITHOUT OVERLAPS) 定義為主鍵,再用 (room_id, PERIOD during) 的外鍵表達明細期間必須由父記錄覆蓋。遷移前清理重疊、空範圍和非法端點;並行插入依賴資料庫約束,應用捕獲衝突並重試或返回可用時段。測試涵蓋相鄰與重疊邊界、父期間拆分、時區轉換和高競爭寫入。

常見錯誤

  • 錯誤: 只在應用層先查再插入。→ 原因: 競爭交易可以同時通過檢查。→ 改進: 用 temporal 約束做最終仲裁。
  • 錯誤: 把範圍欄位放在主鍵前面。→ 原因: 語法和前綴語義要求範圍欄位在最後。→ 改進: 先列業務鍵,再寫 WITHOUT OVERLAPS
  • 錯誤: 認為相鄰區間一定衝突。→ 原因: 結果取決於範圍邊界語義。→ 改進: 統一半開區間並測試端點。
  • 錯誤: 遷移時直接啟用約束。→ 原因: 歷史重疊或空範圍會導致失敗和長鎖。→ 改進: 先審計清理,再分階段發布。

追問與回答

追問 1:[10:00, 11:00)[11:00, 12:00) 會衝突嗎?

在統一半開區間約定下不會,因為 11:00 只屬於第二個區間。若使用閉區間或混合邊界,必須先明確業務規則再建約束。

追問 2:為什麼範圍欄位必須放在最後?

temporal 主鍵按前綴欄位分組,再要求最後的範圍欄位彼此不重疊;把範圍欄位放在前面無法表達「同一實體」的分組語義。

追問 3:期間外鍵是否只檢查一個瞬時點?

不是。PERIOD 表達引用期間需要被父表期間覆蓋,具體組合和邊界應依據 PostgreSQL 18 的約束語義及測試結果確認,不能退化成普通單點外鍵。

追問 4:高並行衝突時如何避免重試風暴?

限制重試次數並加入抖動,先重新讀取可用區間;超過閾值就返回衝突或進入排隊。監控約束錯誤率和鎖等待,必要時按房間分片寫入。

公開來源

同類題目