【问题标题】:Recursive SQL Query with Postgres Ranges To Find Availability使用 Postgres 范围的递归 SQL 查询以查找可用性
【发布时间】:2020-02-18 05:56:40
【问题描述】:

我关注了这篇博文: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

【问题讨论】:

    标签: sql postgresql exclusion-constraint


    【解决方案1】:

    我在原始博客文章中添加了一条评论,说明我认为错误是从哪里引起的,即处理空范围的方式。

    如果日期范围是连续的,或者说是相邻的,则会导致“左”和“右”列中的任何一个,甚至两者都出现“空”范围。现在,在递归 CTE 完成后(并假设空范围在“左”列中),在“左外连接 ... ON ...”子句中,一个免费且有效的 travel_date 与一个 ' empty' range from B.left range since A.left 'empty' && A.left @> 'empty' 因为所有范围都包含空范围。理想情况下,它应该与 NULL 配对,因为这是一个左外连接,以便将其包含在最终结果集中,但“空”有点阻碍。 'empty' 然后在 'GROUP BY ... HAVING ...' 子句中再次弹出,其中 a.left @> 'empty' 评估为 true 并且它被否定,因此所有有效的旅行日期都被丢弃,导致一个空表。我的解决方案如下,将'emptys'设为NULL,并丢弃'center'中的任何日期范围:

    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;
    

    【讨论】:

      【解决方案2】:

      我认为你应该采取另一种方法:

      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;
      

      我们必须递归地将给定范围拆分为左右段,然后只得到那些未被占用的。

      【讨论】:

      • 此函数抛出错误:“错误:在 CASE LINE 10 中不允许设置返回函数:当 $1 @> travel_dates THEN unnest(array[ ^
      • 我在 SQLfiddle 上使用 PostgreSQL 9.6 进行了测试 - sqlfiddle.com/#!17/0ee63/1
      • 感谢您的回答,我可以看到它在 sqlfiddle 上运行,但在本地的 postgres 实例上却没有。与@jkatz05 相同的错误 - NOTICE: drop cascades to 2 other objects DETAIL: drop cascades to table travels drop cascades to function travels_get_available_dates(daterange) ERROR: set-returning functions are not allowed in CASE LINE 31: WHEN $1 @&gt; travel_dates THEN unnest(array[ ^ HINT: You might be able to move the set-returning function into a LATERAL FROM item. SQL state: 0A000 Character: 876
      • 我们需要拆分的左右部分。也许您可以修改查询以使用 2 个 CTE - 一个用于左侧部分,一个用于右侧部分,然后采用并集 - 但我认为这不会产生正确的输出。
      【解决方案3】:

      我最初忘记了“中心”区域的条款。如下:

      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;
      

      【讨论】:

      • 感谢您的回答,尽管在插入 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-18', '2018-03-19', '[]')), (daterange('2018-03-25', '2018-03-27', '[]')); 时似乎错过了 3 日至 6 日之间的可用性,但我看不到这项工作。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-10-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多