在PostgreSQL中查找日期范围之间的交集

Jos*_*itz 5 sql postgresql relational-database window-functions gaps-and-islands

我有两个日期check_in和的记录check_out,我想知道同时签到多个人的范围。

因此,如果我有以下签入/签出:

  • 人A: 1PM - 6PM
  • 人B: 3PM - 10PM
  • 人物C: 9PM - 11PM

我想得到3PM - 6PM(人A和B的9PM - 10PM重叠)和(人B和C的重叠)。

我可以编写一个算法来使用代码在线性时间内做到这一点,是否也可以通过线性时间通过关系查询来做到这一点PostgreSQL

它需要具有最小的响应,这意味着没有重叠的范围。因此,如果有这给了范围的结果6PM - 9PM8PM - 10PM它是不正确的。它应该返回6PM - 10pm

Erw*_*ter 3

假设

该解决方案在很大程度上取决于确切的表定义(包括所有约束)。由于问题中缺乏信息,我将假设此表:

CREATE TABLE booking (
  booking_id serial PRIMARY KEY
, check_in   timestamptz NOT NULL
, check_out  timestamptz NOT NULL
, CONSTRAINT valid_range CHECK (check_out > check_in)
);
Run Code Online (Sandbox Code Playgroud)

因此,没有 NULL 值,只有包含下限和排除上限的有效范围,而且我们并不真正关心签入。

还假设 Postgres 的当前版本至少为9.2

询问

UNION ALL一种仅使用 SQL 使用 a和窗口函数来实现此目的的方法:

SELECT ts AS check_id, next_ts As check_out
FROM  (
   SELECT *, lead(ts) OVER (ORDER BY ts) AS next_ts
   FROM  (
      SELECT *, lag(people_ct, 1 , 0) OVER (ORDER BY ts) AS prev_ct
      FROM  (
         SELECT ts, sum(sum(change)) OVER (ORDER BY ts)::int AS people_ct
         FROM  (
            SELECT check_in AS ts, 1 AS change FROM booking
            UNION ALL
            SELECT check_out, -1 FROM booking
            ) sub1
         GROUP  BY 1
         ) sub2
      ) sub3
   WHERE  people_ct > 1 AND prev_ct < 2 OR  -- start overlap
          people_ct < 2 AND prev_ct > 1     -- end overlap
   ) sub4
WHERE  people_ct > 1 AND prev_ct < 2;
Run Code Online (Sandbox Code Playgroud)

SQL 小提琴。

解释

  • 在子查询中派生出和sub1的表中的一列。给人群加一,减一。check_incheck_outcheck_incheck_out

  • 总而言之,sub2同一时间点的所有事件并使用窗口函数计算运行计数:这是sum()聚合上的窗口函数sum()- 并转换为integer或我们numeric从中得到:

       sum(sum(change)) OVER (ORDER BY ts)::int
    
    Run Code Online (Sandbox Code Playgroud)
  • 查看sub3上一行的计数

  • sub4保留重叠时间范围开始和结束的行,并将时间范围的末尾拉到与 相同的行中lead()

  • 最后,只保留时间范围开始的行。


为了优化性能,我将在 plpgsql 函数中遍历该表一次,如 dba.SE 上的相关答案所示: