Techouse Developers Blog

テックハウス開発者ブログ|マルチプロダクト型スタートアップ|エンジニアによる技術情報を発信|SaaS、求人プラットフォーム、DX推進

Kaigi on Rails 2025 「多重範囲型」の話を振り返ってみた

ogp

はじめに

こんにちは、株式会社 Techouseでエンジニアインターンをしているkinoshitaです。 普段はクラウドハウス労務の開発に携わっています。

本記事では、RIZAPテクノロジーズ株式会社の梅田智大さんによる 「Range on Rails ―「多重範囲型」という新たな選択肢が、複雑ロジックを劇的にシンプルにしたワケ」の内容を振り返ります。加えて、セッションでは触れられなかった性能面について、実際に検証した結果を紹介します。

セッションの概要

「枠に縛られない」予約システム

一般的な予約システムでは「10:00-11:00」「11:00-12:00」といった決まった枠を設けます。しかし、ユーザーが好きな時間を自由に予約できるシステムを作る場合、非常に複雑になり、予約可能時間を算出することが困難になります。

考慮すべき要素:

  • 他のユーザーの予約
  • 店舗の休業日
  • 設備のメンテナンス時間
  • その他の予約不可な期間

これらを全て考慮して「直近1週間の予約可能時間」を計算し、リアルタイムに表示する必要があります。

突破口は「範囲」

この問題に対して、セッションでは範囲という数学的思考を用いて解決するアプローチが紹介されました。 予約不可能な条件は様々な状況が絡み合い、とても複雑な状態に思えますが開始と終了をもつ期間と捉えるとすべて共通の枠組みで考えることができます。

  1. 予約できない期間を「範囲の集合」として捉える
  2. 予約の対象となる期間も同じく「範囲」として扱う
  3. 1,2の範囲の差集合を求める
  4. 残った部分が「予約可能な期間」

この考え方により、複雑なロジックをシンプルな集合演算に落とし込むことができます。

PostgreSQL Range型の限界

この「差集合」のアプローチを実装するために、PostgreSQLのrange型を使おうとしましたが、大きな問題に直面します。

-- 例: 9:00-18:00の間から、12:00-13:00を引く
SELECT tsrange('2025-10-28 09:00', '2025-10-28 18:00') - tsrange('2025-10-28 12:00', '2025-10-28 13:00');
-- エラーが発生します。Range型は1つの連続した範囲しか表現できません

標準のrange型では、差集合を求めた結果が不連続(分断された複数の範囲)になると、エラーが発生してしまいます。

多重範囲型という最適解の発見

そこで辿り着いたのが、PostgreSQL 14で導入された多重範囲型です。 多重範囲型なら、分断された複数の範囲を1つのデータとして扱うことができます。

SELECT tsmultirange(tsrange('2025-10-28 09:00', '2025-10-28 18:00')) - tsmultirange(tsrange('2025-10-28 12:00', '2025-10-28 13:00'));
-- 結果: {["2025-10-28 09:00:00","2025-10-28 12:00:00"),["2025-10-28 13:00:00","2025-10-28 18:00:00")}
-- 複数の範囲を1つのデータとして出力することが可能

多重範囲型を使うことで先ほどの集合演算が可能になり、一連の処理をPostgreSQL側で完結させることができます。

Ruby on Railsでの実装方法

セッションでは、Railsでの実装上の工夫も紹介されていました。

課題: Railsは多重範囲型をサポートしておらず、直接扱うと処理が煩雑になってしまう

解決策: SQLのVIEWを活用する

多重範囲型の複雑さをVIEWに隠蔽し、Rails側からはシンプルに扱えるようにする設計です。以下、データがどのように変換されていくかを図で見ていきます。

ステップ1: 複数のテーブルから予約不可期間を統合

まず、予約・メンテナンス・休業日という異なるテーブルに予約ができない時間のデータが存在する状況を考えてみましょう。

各テーブル

これらをUNION ALLで統合し、「予約不可時間」という共通の概念で扱えるようにします。さらにrange_agg()関数で集約すると、連続する範囲が自動的にマージされ、多重範囲型に変換されます。

UNION ALLとrange_agg

ステップ2: 差集合演算で予約可能時間を計算

対象期間(この例では00:00-24:00)から、予約不可時間の多重範囲を差し引きます。multirange同士の差集合演算(-演算子)により、分断された複数の予約可能時間が1つの多重範囲型として得られます。

差集合演算

ステップ3: unnest()で個別の範囲に展開

最後にunnest()関数で多重範囲を個別の範囲に展開します。これにより、Railsから見ると「1行が1つの予約可能な時間帯」というシンプルなテーブル形式になります。

unnestで展開

VIEWの全体像

ここまでの流れをまとめると、VIEWは以下のような構造になります:

-- 1. 予約不可期間を統合
予約不可期間 AS (
  SELECT 部屋ID, 期間 FROM 予約
  UNION ALL
  SELECT 部屋ID, 期間 FROM メンテナンス
  UNION ALL
  SELECT 部屋ID, 期間 FROM 店舗休業
),

-- 2. range_aggで多重範囲型に集約(自動的にソート・マージされる)
予約不可期間_集約 AS (
  SELECT 部屋ID, range_agg(期間) AS 期間
  FROM 予約不可期間
  GROUP BY 部屋ID
)

-- 3. 対象期間から差し引き、unnestで展開
SELECT 部屋ID, unnest(対象期間 - 期間) AS 予約可能期間
FROM 予約不可期間_集約

range_agg と差集合演算(-)が多重範囲型の核心部分です。

Railsからの利用

このVIEWを使うことで、Rails側では次のようにシンプルな予約可能時間のデータ取得ができます:

# VIEWを通じて予約可能な期間を取得
予約可能時間.where(部屋ID: 1)
# => 部屋1の予約可能時間一覧(Range型のレコード)

Rails側で複雑な計算をする必要がなく、通常のActiveRecordと同じように扱えるのがこの設計の優れた点です。

実際に使ってみる

セッションを聞いて「これは実際に試してみたい」と感じました。同時に、「PostgreSQLで処理が完結するということは、処理速度も変わるのではないか」という仮説が浮かびました。そこで、Rails側で予約可能時間を求める処理をする場合と比較検証を行うことにしました。

検証方針

簡易的な予約システムを構築し、2つの方法で予約可能な時間の検索速度を計測し、比較してみます。

  • 手法1: VIEW(多重範囲型)

    • PostgreSQL側:予約不可期間の集約 → 多重範囲型の演算 → 予約可能な時間を計算
    • Rails側:結果を取得するのみ
  • 手法2: Rails処理

    • PostgreSQL側:予約不可期間をUNION ALLで統合
    • Rails側:ソート → 予約可能な時間を計算

検証条件

  • スタジオ数: 3軒(それぞれが5部屋持つ)
  • 部屋数: 15部屋(予約は各部屋に均等配分)
  • 予約データ: 100件〜15,000件(ユーザーは部屋ごとに予約する)
  • 期間: 1週間
  • 予約不可期間:
    • 予約:5〜20分のランダムな長さ
    • メンテナンス:各部屋で週1回、ランダムな日に2時間発生
    • 休業日:各スタジオ週に1日間
  • 計測方法: 各方式で全部屋分の予約可能時間の計算を3回実行し、平均値を算出

結果

予約件数 VIEW
(多重範囲型)
Rails処理 VIEW vs
Rails処理
100件 0.71ms 0.61ms 0.9倍
200件 0.69ms 0.63ms 0.9倍
500件 0.88ms 0.75ms 0.9倍
1,000件 1.20ms 1.43ms 1.2倍
2,000件 1.40ms 1.76ms 1.3倍
4,000件 2.07ms 3.03ms 1.5倍
8,000件 3.63ms 4.99ms 1.4倍
15,000件 5.07ms 7.37ms 1.5倍

考察

1,000件以上のデータにおいては、仮説通りVIEW方式が高速という結果になりました。

なぜVIEW方式が速いのか

2つの方式の処理フローを比較すると、差が生じるのは主に集約・差集合計算の部分です。

比較項目 VIEW(多重範囲型) Rails処理
データ取得(転送量) ◎ 部屋数分のレコード
(計算済みの空き枠)
○ 部屋数以上のレコード
(統合済みの予約不可期間)
データ統合 ◎ PostgreSQLで計算
(UNION ALL)
◎ PostgreSQLで計算
(UNION ALL)
集約・差集合計算 ◎ PostgreSQLで計算
range_agg, -演算子
△ Rubyで計算

両方式とも、内部的には「ソート → マージ → 差集合計算」という同じ流れで処理していますが、実装の違いにより速度差が生じると推測されます:

  • PostgreSQL側:C言語で実装された関数(range_agg-演算子)
  • Rails側:Rubyインタプリタ上での処理 + ActiveRecordオブジェクトの生成

データ量が増えるほど、この差が累積してVIEW方式の優位性が高まると推測されます。

なお、500件以下の少量データではVIEW方式の方が遅い結果となりましたが、これはVIEWのクエリ自体の複雑さ(UNION ALL + range_agg + 差集合 + unnest)が影響していると推測されます。

まとめ

Kaigi on Rails 2025で得た学びを実際に手を動かして検証することで、多重範囲型の効果を体感できました。

これまでは、アプリケーション側で複雑なロジックを組むのが当然だと思っていた処理が、range_aggや差集合演算子といったSQLの機能を組み合わせるだけで、ここまでシンプルに記述できることは、普段VIEWやSQLを直接書く機会が少ない私にとって、衝撃的な体験でした。 目の前の問題に対し、そこまでメジャーではないものの、この上なく的確に課題を解決する技術(多重範囲型)を見つけ出し、鮮やかに実装する。その発想を今後のエンジニア人生の中で自分も体験できるよう頑張っていこうと思いました。

梅田さん、素晴らしいセッションで多重範囲型の魅力を伝えてくださってありがとうございました。来年のKaigi on Rails 2026にもぜひ参加したいと考えています。


Techouseでは、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。

jp.techouse.com