概要
PostgreSQL に範囲型が来る前から overlaps という便利な演算子があったという事を最近知ったので調べてみたところ、謎の挙動があったので考察します。
-- 片方が1日だけだと true だが
select ('2014-01-01'::date, '2014-01-01'::date)
overlaps ('2014-01-01'::date, '2014-01-31'::date);
-- t
-- 範囲が延びると false
select ('2013-01-01'::date, '2014-01-01'::date)
overlaps ('2014-01-01'::date, '2014-01-31'::date);
-- f
なぜ範囲が1日か2日以上かで判定が変わってしまうのか...
議論
@yancya PostgreSQLでは一般的に範囲は[s,e)です。なので終端日は範囲に入らない。しかし一日しか範囲がないと、起点日sで最初に条件判断してしまうので範囲に入る(入ってしまう)。 仕様が曖昧なのでバグとは言えないと思いますが、範囲型を使うときに注意すべき点ですね。
— jpug_study (@jpug_study) 2014, 7月 11
-- range の && だと全部 true
select daterange('2014-01-01'::date, '2014-01-01'::date, '[]') &&
daterange('2014-01-01'::date, '2014-01-31'::date, '[]');
-- t
select daterange('2013-01-01'::date, '2014-01-01'::date, '[]') &&
daterange('2014-01-01'::date, '2014-01-31'::date, '[]');
-- t
-- date の range で終端が開放の場合は全部 false
select daterange('2014-01-01'::date, '2014-01-01'::date, '[)') &&
daterange('2014-01-01'::date, '2014-01-31'::date, '[)');
-- f
select daterange('2013-01-01'::date, '2014-01-01'::date, '[)') &&
daterange('2014-01-01'::date, '2014-01-31'::date, '[)');
-- f
-- timestamp の range で終端が開放の場合も全部 false
select tsrange('2014-01-01'::timestamp, '2014-01-01'::timestamp, '[)') &&
tsrange('2014-01-01'::timestamp, '2014-01-31'::timestamp, '[)');
-- f
select tsrange('2013-01-01'::timestamp, '2014-01-01'::timestamp, '[)') &&
tsrange('2014-01-01'::timestamp, '2014-01-31'::timestamp, '[)');
-- f
-- between も範囲が1日だからといって判定が変わったりしない(そもそも '[]' だし)
select '2014-01-01'::timestamp between '2014-01-01' and '2014-01-01';
-- t
select '2014-01-01'::timestamp between '2013-01-01' and '2014-01-01';
-- t
うーん... 範囲だからというより、やはり overlaps だけ異端児な気がします...
追記
http://www.postgresql.jp/document/9.3/html/functions-datetime.html
最新のマニュアルで、この現象について言及されてました。
流石 PostgreSQL !ドキュメントが素晴らしい
この式は、2つの時間間隔が重なる(その終端で定義されます)時に真を返します。重ならない場合は偽を返します。終端は日付、時刻、タイムスタンプ、もしくは、日付/時刻/タイムスタンプに続く時間間隔で指定されます。値の組み合わせが与えられた際、開始と終了のいずれを先に記述しても構いません。OVERLAPSは与えられた値の内、若いものを開始として扱います。各時間間隔は、start <=time < endといった半開区間として見なされます。ただし、startとendが同じ値の場合には単一の時間点となります。これは、例えば終了点のみが同じである2つの時間間隔は、必ずしも重ならないことを意味します。
SELECT (DATE '2001-02-16', DATE '2001-12-21') OVERLAPS
(DATE '2001-10-30', DATE '2002-10-30');
Result: true
SELECT (DATE '2001-02-16', INTERVAL '100 days') OVERLAPS
(DATE '2001-10-30', DATE '2002-10-30');
Result: false
SELECT (DATE '2001-10-29', DATE '2001-10-30') OVERLAPS
(DATE '2001-10-30', DATE '2001-10-31');
Result: false
SELECT (DATE '2001-10-30', DATE '2001-10-30') OVERLAPS
(DATE '2001-10-30', DATE '2001-10-31');
Result: true
要するに (a, b) の場合は daterange(a, b, '[)') として扱うけど、(a, a) だったら daterange(a, a, '[]') として扱うからねって事だと思います。
えー。