題目與背景
系統保存房間預訂及其明細。過去由應用先查詢衝突再插入,競爭請求仍可能產生重疊區間。請用 PostgreSQL 18 的 temporal constraints 在資料庫層保證同一房間的期間不重疊,並讓子表引用覆蓋父表的有效期間。
面試官考察什麼
重點是理解 WITHOUT OVERLAPS 用於主鍵或唯一約束的最後一個範圍欄位,PERIOD 用於期間外鍵;資料庫會把相同前綴鍵的非空範圍視為不可重疊集合。答案還要說明半開區間、空範圍、NULL、更新拆分、現有髒資料遷移及並行衝突處理。
先問清楚的澄清問題
時間模型
確認使用 tstzrange 還是 daterange、時區和邊界是否採用半開區間。區間端點和相鄰預訂是否允許接觸會直接影響約束結果。
業務鍵與引用關係
確認房間 ID 是否構成業務前綴、明細期間必須完全落在父期間還是只需存在重疊,以及是否允許一筆預訂跨越多個版本。
遷移與並行
確認舊表是否已有重疊或空區間、遷移窗口和失敗回滾方式。並行插入必須依賴資料庫約束和交易錯誤處理,不能只依賴應用鎖。
30 秒回答框架
「我把房間 ID 作為前綴,把有效期範圍作為最後一欄,用 PRIMARY KEY (roomid, during WITHOUT OVERLAPS) 保證同房間期間不重疊。明細表用 FOREIGN KEY (roomid, PERIOD during) 引用父表的期間鍵。先清理重疊和空範圍,再分階段加約束;並行衝突讓交易捕獲唯一或排他性錯誤並重試,明確半開區間與時區規則。」
深入解答步驟
第一步:選擇範圍型別和邊界
使用適合業務的離散或連續範圍型別,並統一半開區間約定。拒絕空範圍,明確相鄰區間是否可接受;時區時間必須統一轉換,避免夏令時造成意外重疊。
第二步:定義 temporal 主鍵
把實體識別欄位放在前面,範圍欄位放在最後,並使用 WITHOUT OVERLAPS。該約束表達同一實體前綴下的範圍不可重疊,同時繼續提供主鍵的非空和唯一身份語義。
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 主鍵或唯一約束。確認業務需要的是完整覆蓋語義,並測試父記錄拆分或縮短時的級聯行為。
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 並統一半開區間,把 (roomid, during WITHOUT OVERLAPS) 定義為主鍵,再用 (roomid, PERIOD during) 的外鍵表達明細期間必須由父記錄覆蓋。遷移前清理重疊、空範圍和非法端點;並行插入依賴資料庫約束,應用捕獲衝突並重試或返回可用時段。測試涵蓋相鄰與重疊邊界、父期間拆分、時區轉換和高競爭寫入。
常見錯誤
- 錯誤: 只在應用層先查再插入。→ 原因: 競爭交易可以同時通過檢查。→ 改進: 用 temporal 約束做最終仲裁。
- 錯誤: 把範圍欄位放在主鍵前面。→ 原因: 語法和前綴語義要求範圍欄位在最後。→ 改進: 先列業務鍵,再寫
WITHOUT OVERLAPS。 - 錯誤: 認為相鄰區間一定衝突。→ 原因: 結果取決於範圍邊界語義。→ 改進: 統一半開區間並測試端點。
- 錯誤: 遷移時直接啟用約束。→ 原因: 歷史重疊或空範圍會導致失敗和長鎖。→ 改進: 先審計清理,再分階段發布。
追問與回答
追問 1:[10:00, 11:00) 和 [11:00, 12:00) 會衝突嗎?
在統一半開區間約定下不會,因為 11:00 只屬於第二個區間。若使用閉區間或混合邊界,必須先明確業務規則再建約束。
追問 2:為什麼範圍欄位必須放在最後?
temporal 主鍵按前綴欄位分組,再要求最後的範圍欄位彼此不重疊;把範圍欄位放在前面無法表達「同一實體」的分組語義。
追問 3:期間外鍵是否只檢查一個瞬時點?
不是。PERIOD 表達引用期間需要被父表期間覆蓋,具體組合和邊界應依據 PostgreSQL 18 的約束語義及測試結果確認,不能退化成普通單點外鍵。
追問 4:高並行衝突時如何避免重試風暴?
限制重試次數並加入抖動,先重新讀取可用區間;超過閾值就返回衝突或進入排隊。監控約束錯誤率和鎖等待,必要時按房間分片寫入。