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
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
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
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
の計算は必要ない -- 日付や対象範囲で先に重なる候補を絞る 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