アプリで空き時間を照会するだけでは、同時リクエストが同じ空きを見て両方保存する競合が起こり得ます。PostgreSQLのrange型と排他制約を使うと、行をまたぐ重複規則をデータベースに任せられます。一次資料はPostgreSQL 18のrange説明とCREATE TABLE資料、補足にCybertecの専門解説を確認しました。

range型と境界を選ぶ

PostgreSQLには整数、日付、数値、timestamp向けのrangeがあります。timestamp without time zoneにはtsrange、タイムゾーンを考慮した時点にはtstzrangeを選びます。PostgreSQLは後者をUTCとして内部保存し、入力時のタイムゾーン名は保持しません。元の地域時間帯も必要なら、America/New_YorkのようなIANA識別子を別列に保存します。境界には含む・含まないを指定できます。予約で[start, end)の半開区間を使えば、終了時刻と次の開始時刻を一致させられます。業務上の境界と時刻変換を定めてから型を決めます。

重複規則を制約にする

同じ部屋の予約時間を重ねない場合、btree_gistを用意してEXCLUDE USING gist (room WITH =, during WITH &&)のように定義できます。&&は範囲の重なり、=は資源の一致を比較します。すべての比較が成立する行の組み合わせを許さないため、別室の同時予約は許可し、同室の重複は拒否します。適切な演算子クラスと拡張の利用権限が必要です。

NULLや空範囲を別途決める

比較結果がfalseまたはNULLなら、排他制約はその2行を同時に許すことがあります。資源や期間が必須なら別にNOT NULLを指定します。空範囲は他と重ならない場合があり、無限境界は終端なしの区間を表せます。許可する入力を業務規則で決め、禁止する場合は別の制約を設けます。CHECK式がNULLになっても行を拒否しない点にも注意します。

対象バージョンと運用を確認

ここではPostgreSQL 18のrangeとEXCLUDEを前提にしています。WITHOUT OVERLAPSなど新しい構文を旧版へ適用できると考えないでください。パーティション表にはキーに関する制約があります。btree_gistの利用可否、既存データ、マイグレーション時のロックと検証を対象版の資料とステージングで確かめます。事前照会は画面改善には役立ちますが、同時書き込みを守る制約の代わりにはなりません。

宣言例とエラー処理

会議室予約なら、次のようにスキーマで意図を明確にできます。

CREATE EXTENSION btree_gist;
CREATE TABLE reservation (
  room text NOT NULL,
  during tstzrange NOT NULL,
  EXCLUDE USING gist (room WITH =, during WITH &&)
);

これはPostgreSQL文書のroom-range例で両方を必須値にした形です。実行前に拡張権限、サーバー版、タイムゾーン表現を確認し、ステージングで検証します。同室の重複挿入は制約違反です。アプリはこの競合を適切な応答に変換しますが、全DBエラーを予約競合として扱ってはいけません。制約名はログ判別にも役立ちます。

既存行が新しい制約を満たすか事前確認し、予約と関連状態を同時に確定する業務処理だけをトランザクションに含めます。

参考資料