0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PostgreSQLの日時系 Rangeについて学ぶ

Posted at

はじめに

PostgreSQLのRange型について学んでいこうと思います!!
今回は日時のみを対象に学んでいます!

PostgreSQLのRange型について

PostgreSQLではRange型という物が存在します。最近まで、私もこんなカラムがあるんだと知らなかったんですが😅
調べてみると、Range型は3つ存在しているようです。
※ちなみにPostgreSQL 14からはmultirange型が追加されています。触ってみたんですが、そこまでは必要ないかなと感じたので、今回の対象外としています

型名 要素の型 格納される値 タイムゾーン情報 主な用途 代表的な落とし穴
daterange date 日単位(0 時開始) なし 休暇・請求期間・学期など “日付だけ” で完結する区間 同日に複数イベントを区別できない
tsrange timestamp without time zone 「壁時計」の時刻 なし 社内など 単一タイムゾーンで完結する日程管理 DST 移行日に 存在しない時刻 が入る/DB TimeZone 変更で解釈ズレ
tstzrange timestamptz (内部 UTC) UTC に換算した絶対時刻 あり(自動で UTC↔ローカル変換) 国際利用・複数 TZ を跨ぐカレンダー/予約システム 取得時に必ず AT TIME ZONE が必要でクエリが冗長

ごにょごにょ上に書いたんですが、日付のみならばdaterange、日本のみで利用する場合はtsrange,世界標準で利用する場合は、tstzrangeのようです。今回はtsrangeのみを対象とします

Hand-on

-- tsrange のラッパー型を定義
CREATE DOMAIN bounded_tsrange AS tsrange
  CHECK (
    NOT lower_inf(VALUE)
    AND NOT upper_inf(VALUE)
    AND NOT isempty(VALUE)
  );

-- テーブル定義で直接使用
CREATE TABLE calendar_events (
  id         serial PRIMARY KEY,
  title      text             NOT NULL,
  time_range bounded_tsrange  NOT NULL
);
SQLの成功と失敗例
-- OK: 境界が両方ある
INSERT INTO calendar_events (title, time_range)
VALUES ('OK例', '[2025-05-15 10:00, 2025-05-15 12:00)'::tsrange);

-- NG: 上端なし
INSERT INTO calendar_events (title, time_range)
VALUES ('NG例', '[2025-05-15 10:00,)'::tsrange);
-- ERROR:  new row for relation "calendar_events" violates check constraint "chk_time_range_bounded"

-- NG: 下端なし
INSERT INTO calendar_events (title, time_range)
VALUES ('NG例2', '(, 2025-05-15 12:00]'::tsrange);

-- NG: 空区間
INSERT INTO calendar_events (title, time_range)
VALUES ('NG例3', '[2025-05-15 10:00, 2025-05-15 10:00)'::tsrange);

開区間、閉区間を学ぶ

境界値を含むかどうかは開区間、閉区間で定義しています。
例えば、9時〜10時で実施されているイベントを調べるとします。
データは下記のように用意します。

ID 区間表記
(tsrange 風)
開始 終了 端点 9:00–10:00 と
重複?
A [07:00, 08:30] 07:00 08:30 閉/閉
B [08:30, 09:00] 08:30 09:00 閉/閉
C (08:20, 09:00) 08:20 09:00 開/開
D [08:30, 09:30] 08:30 09:30 閉/閉
E [09:30, 10:00] 09:30 10:00 閉/閉
F [10:00, 10:30] 10:00 10:30 閉/閉
G [10:30, 11:00] 10:30 11:00 閉/閉

image.png

ここで注目するのは、BとCです。今回、検索の対象は9時〜10時に少しでも引っかかるものを対象としました。
Bは終端が10時の閉区間、Cは終端が10時の開区間。Cは境界値の10時を含まないが、Bは境界値の10時を含んでいるため、検索結果に影響がありました。

SQLの結果を確認する

------------------------------------------------------------
-- データ投入(9:00-10:00 を基準にした 8 本)
------------------------------------------------------------
INSERT INTO calendar_events (title, time_range) VALUES
-- A 07:00–08:30  (閉区間)
('A', '[2025-05-15 07:00, 2025-05-15 08:30]'::tsrange),

-- B 08:30–09:00  (閉区間)
('B', '[2025-05-15 08:30, 2025-05-15 09:00]'::tsrange),

-- C 08:20–09:00  (開区間)
('C', '(2025-05-15 08:20, 2025-05-15 09:00)'::tsrange),

-- D 08:30–09:30  (閉区間)
('D', '[2025-05-15 08:30, 2025-05-15 09:30]'::tsrange),

-- E 09:30–10:00  (閉区間)
('E', '[2025-05-15 09:30, 2025-05-15 10:00]'::tsrange),

-- F 10:00–10:30  (閉区間)
('F', '[2025-05-15 10:00, 2025-05-15 10:30]'::tsrange),

-- G 10:30–11:00  (閉区間)
('G', '[2025-05-15 10:30, 2025-05-15 11:00]'::tsrange);

→データを準備します。

calendar=# SELECT *
FROM   calendar_events
WHERE  time_range && '[2025-05-15 09:00, 2025-05-15 10:00]'::tsrange;
 id | title |                  time_range                   
----+-------+-----------------------------------------------
  4 | D     | ["2025-05-15 08:30:00","2025-05-15 09:30:00"]
  2 | B     | ["2025-05-15 08:30:00","2025-05-15 09:00:00"]
  6 | F     | ["2025-05-15 10:00:00","2025-05-15 10:30:00"]
  5 | E     | ["2025-05-15 09:30:00","2025-05-15 10:00:00"]
(4 rows)

→開始と終端を含む、9時〜10時での検索は上のようになります。

calendar=# SELECT *
FROM   calendar_events
WHERE  time_range && '(2025-05-15 09:00, 2025-05-15 10:00)'::tsrange;
 id | title |                  time_range                   
----+-------+-----------------------------------------------
  4 | D     | ["2025-05-15 08:30:00","2025-05-15 09:30:00"]
  5 | E     | ["2025-05-15 09:30:00","2025-05-15 10:00:00"]
(2 rows)

→開始と終端を含まない、9時〜10時での検索は上のようになります。
開区間、閉区間はデータの挿入時、検索時にも意識する必要があります

Appendix

では、実際にGoogleカレンダーやOffice365のカレンダーはどうやって開区間、閉区間を定義しているのでしょうか? iCalendar(RFC 5545)の仕様に沿って定義しているようです。

・カレンダー実装での区間の持ち方

ケース 典型的な保存形式 採用理由
日時付きイベント
(会議・予約など)
[start, end)
右端排他
隣接イベントが衝突しない/
長さ = end - start の単純計算
終日イベント [start-date, next-date)
例) 5/15 の終日は DTSTART:20250515, DTEND:20250516
iCalendar (RFC 5545) の標準運用

・Google カレンダーの実例(ics 抜粋)

DTSTART:20250515T090000Z   → 9:00 開始
DTEND:20250515T100000Z     → 10:00 は **含まない**

多くのカレンダー/予約システムは [start, end)(右端排他)がデフォルトで定義されているようです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?