Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

chocoZAPの 予約システムを作った話 2

Slide 3

Slide 3 text

3 一般的な予約システムの仕組み ■ 固定の「予約枠」を設けておく ■ ユーザーは枠を選んで予約する

Slide 4

Slide 4 text

4 chocoZAPの予約システムの特徴 24時間無人営業 スキマ時間に気軽に使ってほしい 「好きな時間を自由に予約できる」ようにしたい つまり・・ 枠に縛られない 予約システム!

Slide 5

Slide 5 text

5 枠がない予約システムの難しさ 予約枠あり 予約枠なし

Slide 6

Slide 6 text

6 枠がなくても、空き状況は一覧で見たい 枠という実態がないので、 空き状況の一覧化は 至難の業

Slide 7

Slide 7 text

7 突破口は「範囲」 予約できない期間を 範囲の集合 として捉え、 対象となる期間との 差集合 を求めることで、 予約可否を判定する

Slide 8

Slide 8 text

8 自己紹介 初心者向けコンビニジム「chocoZAP」の システム開発 / 運用を担当しています 梅田 智大 プロダクト開発統括1部 バックエンドユニット R IZ APテクノロジーズ株式会社 Umeda Tomohiro

Slide 9

Slide 9 text

9 こんなプロダクトを開発しています! 全国の店舗の リアルタイム混雑度の可視化 予約システムの バックエンドAPI IoT連携による 店舗資源の最適化 認証認可基盤 顧客基盤 ※イラストはイメージです

Slide 10

Slide 10 text

本題に戻ります 10

Slide 11

Slide 11 text

11 「範囲の集合」と捉えるとは? 複雑に見えていた予約不可条件も捉え直してみると、 「開始と終了を持つ期間」という共通項を発見! 改めて予約できない条件を考える ◼ すでに別の予約が入っている ◼ 店舗が休業している ◼ ルームがメンテナンス中 ✕ 2025/9/27 10:00〜11:00 ✕ 2025/9/28〜9/29 ✕ 2025/9/27 02:00〜09:00

Slide 12

Slide 12 text

12 "予約できない期間"を「範囲」として扱う 予約できない期間

Slide 13

Slide 13 text

13 "対象となる期間"も「範囲」として扱う 対象となる期間 予約できない期間

Slide 14

Slide 14 text

14 範囲の"差集合"を求める 対象となる期間 予約できない期間

Slide 15

Slide 15 text

15 "差集合"を求めた結果 残った範囲が 予約が可能な期間 となる! 予約可能な期間

Slide 16

Slide 16 text

16 PostgreSQLのRange型では 分断された範囲 を扱えない ◼ 差集合の結果は、分断された複数の範囲となる tsrange('2025-09-26 12:00', '2025-09-26 13:00') - tsrange('2025-09-26 12:10', '2025-09-26 12:40'); -- 結果は「12:00〜12:10」と「12:40〜13:00」に分断される -- 複数の範囲はrange型では扱えずエラーとなる ERROR: result of range difference would not be contiguous ◼ Range型ではひとつの連続した範囲しか扱えないのでエラーになる -- 例: 「12:00〜13:00」から「12:10〜12:40」を引く

Slide 17

Slide 17 text

17 "多重範囲型"という最適解 そこで辿り着いたのが、多重範囲型 (multirange) 分断された複数の範囲を、 1つのデータとして扱える

Slide 18

Slide 18 text

18 PostgreSQLの「多重範囲型」とは ◼ 複数の範囲をArrayのようにまとめて扱えるデータ型 ◼ 範囲の種類によって複数の型がある Int4multirange 整数の多重範囲 tsmultirange タイムスタンプの多重範囲 datemultirange 日付の多重範囲 1..10, 13..15, 30..33, 37..40, 50..100, 110..112 2025-09-26 9:00..2025- 09-26 12:00, 2025-09-26 14:00.. 2025-09-27 18:00 2025-09-01..2025-09-05, 2025-02-10..2025-02-12, 2025-09-26..2025-09-27

Slide 19

Slide 19 text

19 多重範囲型は範囲型と同様の演算を利用できる ◼ 範囲型と同じように 重なり・積集合・差集合 が扱える! -- 重なり(overlaps) SELECT int4multirange(1, 10) && int4multirange(5, 15); -- 結果: t -- 積集合(intersection) SELECT int4multirange(1, 10) * int4multirange(5, 15); -- 結果: {[5, 10)} -- 差集合(difference) SELECT int4multirange(1, 10) - int4multirange(5, 7); -- 結果: {[1, 5), [7, 10)}

Slide 20

Slide 20 text

20 「多重範囲型」を利用して予約の空き状況一覧を作成! 予約不可な複数の期間を多重範囲に集約 1 2 3 空き状況を確認する対象期間も多重範囲に変換 予約可能な期間を差集合で算出!

Slide 21

Slide 21 text

21 予約不可な複数の期間を多重範囲に集約 ◼ 既存の予約期間、店舗の休業期間、ルームのメンテナンス期間… などの、予約不可な複数の期間をひとつの多重範囲に集約していきます -- コード例(イメージ) tsmultirange(‘{ [2025-09-21 10:30, 2025-09-21 11:00), [2025-09-22 00:00, 2025-09-24 22:00), [2025-09-25 14:00,2025-09-25 15:30), ... }’) -- 結果: {["2025-09-21 10:30:00","2025-09-21 11:00:00"),["2025-09-22 00:00:00","2025-09-24 22:00:00"),["2025-09-25 14:00:00","2025-09-25 15:30:00"), ...} 1

Slide 22

Slide 22 text

22 -- コード例(イメージ) tsmultirange(‘{ [2025-09-21 00:00, 2025-09-28 00:00) }’) -- 結果: {["2025-09-21 00:00:00","2025-09-28 00:00:00")} 空き状況を確認する対象期間も多重範囲に変換 2 ◼ 差集合の演算は多重範囲同士でのみ可能なため、 空き状況を確認する対象期間も多重範囲に変換します

Slide 23

Slide 23 text

23 -- コード例(イメージ) tsmultirange(1週間の多重範囲) – tsmultirange(予約不可期間の多重範囲) -- 結果: { ["2025-09-21 00:00:00","2025-09-21 10:30:00"), ["2025-09-21 11:00:00","2025-09-22 00:00:00"), ["2025-09-24 22:00:00","2025-09-25 14:00:00"), ... } 予約可能な期間を差集合で算出! ◼ 空き状況を確認する対象期間の多重範囲と、 予約不可な期間の多重範囲の差集合を算出! ◼ 差集合の結果が、予約可能な期間となる 3

Slide 24

Slide 24 text

24 「多重範囲型」の活用シーンは様々! 複数の雑多な期間を集計する場面で、 多重範囲型 (multirange) は 威力を発揮する かもしれない ◼ サブスクリプションの利用期間 (休止・再開を含む複数期間) ◼ 勤怠シフト管理 (勤務・休憩・残業の時間帯を集合で管理) ◼ コンテンツ配信/視聴時間の集計 (視聴セッションの多区間集約) ◼ 在庫や貸出管理 (貸出・返却で空き期間を差集合で算出) ◼ システム障害の発生 〜 復旧時間の管理 (障害発生率の算出など) ヒント

Slide 25

Slide 25 text

Ruby on Railsで 「多重範囲型」を扱う方法 25

Slide 26

Slide 26 text

26 ◼ 「多重範囲型」は複数の範囲をひとまとめにしたデータ型 ◼ 個別の範囲に展開することで扱いやすくなる 多重範囲型は展開すると扱いやすい

Slide 27

Slide 27 text

27 PostgreSQLの便利関数を使って範囲を変換!

Slide 28

Slide 28 text

28 ◼ 各レコードは範囲型で保持 ◼ 集計時に多重範囲型へ集約 ◼ 結果を展開して範囲型として扱うとコードがシンプル 多重範囲は集計専用で使うのがオススメ!

Slide 29

Slide 29 text

29 ◼ 多重範囲型(multirange)はRailsでサポートされていない 無理にActiveRecordで扱うとコードが複雑になりがち ◼ 複雑な集計処理はSQLのVIEWに閉じ込めてしまう 多重範囲型(multirange)の扱いはSQLで完結 ◼ VIEWに基づいたModelを作成することでコードをシンプルに 多重範囲をRailsでは意識せずに扱える SQL VIEWを利用してコードをシンプルに

Slide 30

Slide 30 text

30 VIEWを利用して集計した例 -- 予約できない期間群 → multirangeに集約 → 差集合で空き期間を集計 → multirangeをrangeに展開 CREATE VIEW reservation_availabilities AS WITH -- 1) 予約できない期間をUNIONでひとつの表にまとめる reservation_unavailabilities AS ( -- 既存の予約 SELECT room_id, period FROM reservations UNION ALL -- ルームのメンテナンス SELECT room_id, period FROM room_reservation_unavailabilities UNION ALL -- 店舗の休業 SELECT r.id AS room_id, sr.period FROM studio_reservation_unavailabilities sr JOIN rooms r ON r.studio_id = sr.studio_id ), -- 2) range_aggで予約不可を「多重範囲(multirange)」に集約 multi_reservation_unavailabilities AS ( SELECT room_id, range_agg(period) AS multi_period FROM reservation_unavailabilities GROUP BY room_id ), -- 3) 1週間の範囲 - 予約不可範囲 = 予約可能範囲 reservation_availabilities AS ( SELECT room_id, tsmultirange( tsrange(current_date, current_date + interval '7 days’) ) - multi_period AS multi_period FROM multi_reservation_availabilities ) -- 4) multirange を unnest で展開して、個々の空き期間(tsrange)にする SELECT room_id, unnest(multi_period) AS period FROM reservation_availabilities;

Slide 31

Slide 31 text

31 VIEWに対応するModelを作ればコードはシンプルに! # app/models/reservation_availability.rb class ReservationAvailability < ApplicationRecord belongs_to :room end # app/models/room.rb class Room < ApplicationRecord has_many :reservation_availabilities end # 使い方 room = Room.find(params[:id]) room.reservation_availabilities # => 予約可能な期間の一覧

Slide 32

Slide 32 text

32 まとめ ◼ 範囲という数理的思考を用いれば、コードは劇的に シンプル に ◼ 多重範囲型は集計専用とし、 結果を展開して範囲型にする ことでRailsで扱いやすくなる ◼ 多重範囲型を使えば、 分断された複数の範囲を扱える