Upgrade to Pro — share decks privately, control downloads, hide ads and more …

multirange 型(多重範囲型)の活用

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on. →
Avatar for sunnyone sunnyone
November 20, 2025

multirange 型(多重範囲型)の活用

Avatar for sunnyone

sunnyone

November 20, 2025

More Decks by sunnyone

Other Decks in Programming

Transcript

  1. アジェンダ 1. range 型 / multirange 型とは 2. 何が便利なのか? 3.

    予約管理での使用例 空き枠の管理 カレンダーの生成 予約の重複防止 4. Tips 3
  2. range 型とは 連続した区間を1 つの値で表す型 例 10 〜20 (intrange) 11/21 10:00

    〜 11/22 20:00 (tsrange) 開区間/ 閉区間を保持(例: [a,b) ) 5
  3. multirange の何が便利なのか? 複数のrange をまとめた値で表現することでさらなる演算が可能 多重範囲(multirange) に集約すると歯抜けとなった範囲もひとつ の値として扱える 例:09:00 〜10:00, 12:00

    〜13:00 差・積で空き検索や絞り込みがシンプルなSQL に 差 - :重なる範囲を取り除く 09:00 〜17:00 から 10:00-11:00 を引いたら? 積 * :共通部分で絞り込む 09:00 〜17:00 と 13:00 〜20:00 、 15:00 〜21:00 の共通部分 は? 8
  4. スキーマ例(FK 等省略) CREATE EXTENSION IF NOT EXISTS btree_gist; -- 枠

    CREATE TABLE reservation_slot ( id bigserial PRIMARY KEY, available tstzrange NOT NULL ); -- 予約 CREATE TABLE reservation ( id bigserial PRIMARY KEY, reservation_slot_id bigint NOT NULL, period tstzrange NOT NULL, EXCLUDE USING gist ( reservation_slot_id WITH =, period WITH &&) DEFERRABLE INITIALLY DEFERRED ); 10
  5. 空き枠と予約の挿入 つながった区間が重複しないよう終端は「) 」で開いておく INSERT INTO reservation_slot (available) VALUES ('[2025-10-20 09:00:00+09,2025-10-20

    12:00:00+09)'::tstzrange); INSERT INTO reservation (reservation_slot_id, period) VALUES (1, '[2025-10-20 09:00:00+09,2025-10-20 10:00:00+09)'::tstzrange); 11
  6. 特定の期間の予約の一覧を知りたい && を使うと範囲に重なりがあるレコードを抽出できる 例:10/20 の予約の一覧を得たい -- 指定時間帯に重なる予約を検索 SELECT * FROM

    reservation r WHERE r.period && tstzrange(:start, :end, '[)'); tstzrange() は範囲をパラメータで渡すときに便利 時刻の組に開閉の指定 [) を与えられる 12
  7. 予約をかぶらせたくない 排他制約を使うことで時間帯の重複を防ぐことができる 例: EXCLUDE USING gist (slot_id WITH =, period

    WITH &&) INSERT INTO reservation(reservation_slot_id, period) VALUES (1, tstzrange('2025-09-22 10:00+09', '2025-09-22 10:30+09', '[)')); -- EXCLUDE USING gist 〜により挿入が防がれる INSERT INTO reservation(reservation_slot_id, period) VALUES (1, tstzrange('2025-09-22 10:15+09', '2025-09-22 10:45+09', '[)')); 13
  8. 予約の空き時間を計算したい 範囲に重なる「枠」と「予約」を絞り込む( && を使用) 枠ごとに予約済み範囲の差分を取っておく( - を使用) 例:09:00 〜17:00 -

    {10:00 〜11:00 、15:00 〜16:00 }- ① カレンダーのセルの部分を生成する( generate_series を使用) 例:09:00 〜10:00, 10:00 〜11:00, 11:00 〜12:00, ... カレンダーのセル部分がそれぞれ、①の空き時間帯に含まれている かどうか確認する( <@ を使用) 例:09:00 〜10:00 OK, 10:00 〜11:00 NG 、11:00 〜12:00 OK, ... 14
  9. カレンダーの生成SQL WITH vacancy AS ( SELECT rs.id, rs.available, (tstzmultirange(rs.available) -

    coalesce(range_agg(r.period), tstzmultirange())) AS vacant_range FROM reservation_slot rs LEFT OUTER JOIN reservation r ON rs.id = r.reservation_slot_id AND r.period && tstzrange(:start_time, :end_time, '[)') WHERE rs.available && tstzrange(:start_time, :end_time, '[)') GROUP BY rs.id, rs.available ), calendar_item AS ( SELECT tstzrange(t, t + '30 minutes'::interval) AS period FROM generate_series(:start_time, :end_time, '30 minutes') t ) SELECT lower(c.period) AS start, EXISTS( SELECT 1 FROM vacancy v WHERE c.period <@ v.vacant_range ) AS vacant FROM calendar_item c 16
  10. 契約条件など別の条件で絞り込みたい ユーザーのプランによる制限など、別の制約も積( * )によって重ね られる。 -- 指定ウィンドウとの共通部分だけを提示(固定値の例) WITH ( 指定略)

    SELECT lower(c.period) AS start, EXISTS( SELECT 1 FROM vacancy v WHERE c.period <@ (v.vacant_range * tstzmultirange( '[2025-10-10 00:00:00+0900, 2025-10-11 00:00:00+0900)'::tstzrange)) ) AS vacant FROM calendar_item c 17
  11. 予約の重複防止(パターンB ) :CONSTRAINT TRIGGER COMMIT 時 に 業務ルールをまとめて検証 CREATE CONSTRAINT

    TRIGGER reservation_check AFTER INSERT OR UPDATE ON reservation DEFERRABLE INITIALLY DEFERRED FOR EACH STATEMENT EXECUTE FUNCTION verify_reservations(); CREATE OR REPLACE FUNCTION verify_reservations() -- 実体略 21
  12. 行を減らすための && 厳密な計算の前に、 && で先にざっくり削れたりする range_agg 対象の削減 例:10/1 〜10/31 を知りたいのに11/1

    の計算は必要ない -- 日付や対象範囲で先に重なる候補を絞る SELECT reservation_slot_id, COALESCE(range_agg(period), tstzmultirange()) AS used_ranges FROM reservation WHERE period && tstzrange(:day_start, :day_end, '[)') GROUP BY reservation_slot_id 22