[英]Recursive SQL Query with Postgres Ranges To Find Availability
我关注了这篇博文: https ://info.crunchydata.com/blog/range-types-recursion-how-to-search-availability-with-postgresql
CREATE TABLE travels (
id serial PRIMARY KEY,
travel_dates daterange NOT NULL,
EXCLUDE USING spgist (travel_dates WITH &&)
);
当我插入持续时间背靠背的行时,发现这个函数有问题
CREATE OR REPLACE FUNCTION travels_get_available_dates(daterange)
RETURNS TABLE(available_dates daterange)
AS $$
WITH RECURSIVE calendar AS (
SELECT
$1 AS left,
$1 AS center,
$1 AS right
UNION
SELECT
CASE travels.travel_dates && calendar.left
WHEN TRUE THEN daterange(lower(calendar.left), lower(travels.travel_dates * calendar.left))
ELSE daterange(lower(calendar.right), lower(travels.travel_dates * calendar.right))
END AS left,
CASE travels.travel_dates && calendar.left
WHEN TRUE THEN travels.travel_dates * calendar.left
ELSE travels.travel_dates * calendar.right
END AS center,
CASE travels.travel_dates && calendar.right
WHEN TRUE THEN daterange(upper(travels.travel_dates * calendar.right), upper(calendar.right))
ELSE daterange(upper(travels.travel_dates * calendar.left), upper(calendar.left))
END AS right
FROM calendar
JOIN travels ON
travels.travel_dates && $1 AND
travels.travel_dates <> calendar.center AND (
travels.travel_dates && calendar.left OR
travels.travel_dates && calendar.right
)
)
SELECT *
FROM (
SELECT
a.left AS available_dates
FROM calendar a
LEFT OUTER JOIN calendar b ON
a.left <> b.left AND
a.left @> b.left
GROUP BY a.left
HAVING NOT bool_or(COALESCE(a.left @> b.left, FALSE))
UNION
SELECT
a.right AS available_dates
FROM calendar a
LEFT OUTER JOIN calendar b ON
a.right <> b.right AND
a.right @> b.right
GROUP BY a.right
HAVING NOT bool_or(COALESCE(a.right @> b.right, FALSE))
) a
$$ LANGUAGE SQL STABLE;
INSERT INTO travels (travel_dates)
VALUES
(daterange('2018-03-02', '2018-03-02', '[]')),
(daterange('2018-03-06', '2018-03-09', '[]')),
(daterange('2018-03-11', '2018-03-12', '[]')),
(daterange('2018-03-16', '2018-03-17', '[]')),
(daterange('2018-03-25', '2018-03-27', '[]'));
这在这一点上按预期工作。
SELECT *
FROM travels_get_available_dates(daterange('2018-03-01', '2018-04-01'))
ORDER BY available_dates;
available_dates
-------------------------
[2018-03-01,2018-03-02)
[2018-03-03,2018-03-06)
[2018-03-10,2018-03-11)
[2018-03-13,2018-03-16)
[2018-03-18,2018-03-25)
[2018-03-28,2018-04-01)
但是当添加这一行时:
INSERT INTO travels (travel_dates)
VALUES
(daterange('2018-03-03', '2018-03-05', '[]'));
并重新运行
SELECT *
FROM travels_get_available_dates(daterange('2018-03-01', '2018-04-01'))
ORDER BY available_dates;
我得到
available_dates
-------------------------
empty
我在原始博客文章中添加了一条评论,说明我认为错误的来源,即处理空范围的方式。
如果日期范围是连续的或相邻的,则会导致“左”和“右”列中的任一列或什至两者中的“空”范围。 现在,在递归 CTE 完成后(并假设空范围在“左”列中),在“LEFT OUTER JOIN ... ON ...”子句中,一个免费且有效的 travel_date 与一个 ' 配对来自 B.left 范围的空'范围,因为 A.left <> 'empty' && A.left @> 'empty' 因为所有范围都包含空范围。 理想情况下,它应该与 NULL 配对,因为这是一个左外连接,可以将其包含在最终结果集中,但“空”有点碍事。 'empty' 然后再次出现在 'GROUP BY ... HAVING ...' 子句中,其中 a.left @> 'empty' 评估为 true 并且它被否定,因此所有有效的旅行日期都被丢弃,导致一个空表。 我的解决方案如下,将“空”设为 NULL,并丢弃“中心”中的任何日期范围:
CREATE OR REPLACE FUNCTION travels_get_available_dates(daterange)
RETURNS TABLE(available_dates daterange)
AS $$
WITH RECURSIVE calendar AS (
SELECT
$1 AS left,
$1 AS center,
$1 AS right
UNION
SELECT
CASE travels.travel_dates && calendar.left
WHEN TRUE THEN daterange(lower(calendar.left), lower(travels.travel_dates * calendar.left))
ELSE daterange(lower(calendar.right), lower(travels.travel_dates * calendar.right))
END AS left,
CASE travels.travel_dates && calendar.left
WHEN TRUE THEN travels.travel_dates * calendar.left
ELSE travels.travel_dates * calendar.right
END AS center,
CASE travels.travel_dates && calendar.right
WHEN TRUE THEN daterange(upper(travels.travel_dates * calendar.right), upper(calendar.right))
ELSE daterange(upper(travels.travel_dates * calendar.left), upper(calendar.left))
END AS right
FROM calendar
JOIN travels ON
travels.travel_dates && $1 AND
travels.travel_dates <> calendar.center AND (
travels.travel_dates && calendar.left OR
travels.travel_dates && calendar.right
)
)
SELECT *
FROM (
SELECT
a.left AS available_dates
FROM calendar a
LEFT OUTER JOIN calendar b ON
a.left <> b.left AND
a.left @> b.left
GROUP BY a.left
HAVING NOT bool_or(coalesce(a.left @> case when isempty(b.left) then null else b.left end, FALSE))
UNION
SELECT
a.right AS available_dates
FROM calendar a
LEFT OUTER JOIN calendar b ON
a.right <> b.right AND
a.right @> b.right
GROUP BY a.right
HAVING NOT bool_or(coalesce(a.right @> case when isempty(b.right) then null else b.right end, false))
EXCEPT
SELECT a.center AS available_dates
FROM calendar a
LEFT OUTER JOIN calendar b ON
a.center <> b.center AND
a.center @> b.center
GROUP BY a.center
HAVING NOT bool_or(COALESCE(a.center @> b.center, FALSE))
) a
WHERE NOT isempty(a.available_dates)
$$ LANGUAGE SQL STABLE;
我认为你应该采取另一种方法:
CREATE OR REPLACE FUNCTION travels_get_available_dates(daterange)
RETURNS TABLE(
available_dates daterange
)
AS $$
WITH RECURSIVE calendar(available_dates) AS
(
SELECT
CASE
WHEN $1 @> travel_dates THEN unnest(array[
daterange(lower($1),lower(travel_dates)),
daterange(upper(travel_dates),upper($1))
])
WHEN lower($1) < lower(travel_dates) THEN daterange(lower($1),lower(travel_dates))
WHEN upper($1) > upper(travel_dates) THEN daterange(upper(travel_dates),upper($1))
END
FROM travels
WHERE $1 && travel_dates AND NOT travel_dates @> $1
UNION
SELECT
CASE
WHEN available_dates @> travel_dates THEN unnest(array[
daterange(lower(available_dates),lower(travel_dates)),
daterange(upper(travel_dates),upper(available_dates))
])
WHEN lower(available_dates) < lower(travel_dates) THEN daterange(lower(available_dates),lower(travel_dates))
WHEN upper(available_dates) > upper(travel_dates) THEN daterange(upper(travel_dates),upper(available_dates))
END
FROM travels
JOIN calendar ON available_dates && travel_dates AND NOT travel_dates @> available_dates
)
SELECT $1 AS available_dates
WHERE NOT EXISTS(SELECT 1 FROM travels WHERE travel_dates <@ $1)
UNION
SELECT * FROM calendar
WHERE $1 <> available_dates AND 'empty' <> available_dates
AND NOT EXISTS(SELECT 1 FROM travels WHERE available_dates && travel_dates)
$$ LANGUAGE SQL STABLE;
我们必须递归地分割左右段上的给定范围,然后只得到那些未被占用的范围。
我最初忘记了“中心”区域的条款。 下面是:
CREATE OR REPLACE FUNCTION travels_get_available_dates(daterange)
RETURNS TABLE(available_dates daterange)
AS $$
WITH RECURSIVE calendar AS (
SELECT
$1 AS left,
$1 AS center,
$1 AS right
UNION
SELECT
CASE travels.travel_dates && calendar.left
WHEN TRUE THEN daterange(lower(calendar.left), lower(travels.travel_dates * calendar.left))
ELSE daterange(lower(calendar.right), lower(travels.travel_dates * calendar.right))
END AS left,
CASE travels.travel_dates && calendar.left
WHEN TRUE THEN travels.travel_dates * calendar.left
ELSE travels.travel_dates * calendar.right
END AS center,
CASE travels.travel_dates && calendar.right
WHEN TRUE THEN daterange(upper(travels.travel_dates * calendar.right), upper(calendar.right))
ELSE daterange(upper(travels.travel_dates * calendar.left), upper(calendar.left))
END AS right
FROM calendar
JOIN travels ON
travels.travel_dates && $1 AND
travels.travel_dates <> calendar.center AND (
travels.travel_dates && calendar.left OR
travels.travel_dates && calendar.right
)
)
SELECT *
FROM (
SELECT
a.left AS available_dates
FROM calendar a
LEFT OUTER JOIN calendar b ON
a.left <> b.left AND
a.left @> b.left
GROUP BY a.left
HAVING NOT bool_or(COALESCE(a.left @> b.left, FALSE))
UNION
SELECT a.center AS available_dates
FROM calendar a
LEFT OUTER JOIN calendar b ON
a.center <> b.center AND
a.center @> b.center
GROUP BY a.center
HAVING NOT bool_or(COALESCE(a.center @> b.center, FALSE))
UNION
SELECT
a.right AS available_dates
FROM calendar a
LEFT OUTER JOIN calendar b ON
a.right <> b.right AND
a.right @> b.right
GROUP BY a.right
HAVING NOT bool_or(COALESCE(a.right @> b.right, FALSE))
) a
WHERE NOT isempty(a.available_dates)
$$ LANGUAGE SQL STABLE;
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.