【问题标题】:Find Intersection Between Date Ranges In PostgreSQL在 PostgreSQL 中查找日期范围之间的交集
【发布时间】:2016-04-14 14:37:24
【问题描述】:

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

所以,如果我有以下签入/结账:

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

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

我可以用代码编写一个算法在线性时间内执行此操作,是否也可以通过PostgreSQL 的线性时间关系查询来执行此操作?

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

【问题讨论】:

  • 您的 Postgres 版本、您的确切表定义(完整的 CREATE TABLE 脚本,包括所有约束或您在 psql 中使用 \d tbl 获得的内容)和一些示例数据会很不错。
  • 是的,版本会帮助我们回答,最近的版本添加了可能适用的新日期范围功能。
  • 我想解决方案将涉及窗口函数和可能的递归 CTE

标签: sql postgresql relational-database window-functions gaps-and-islands


【解决方案1】:

假设

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

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)
);

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

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

查询

使用UNION ALL 和窗口函数仅使用 SQL 的一种方法:

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;

SQL Fiddle.

说明

  • 在子查询sub1 中,在一个列中派生check_incheck_out 的表。 check_in 在人群中加一,check_out 减一。

  • sub2 中,对同一时间点的所有事件求和,并使用窗口函数计算运行计数:这是窗口函数 sum() 在聚合 sum() 上 - 并转换为 integer 或者我们得到 @ 987654335@来自此:

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

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

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


为了优化性能我会在 plpgsql 函数中遍历表 once,就像在 dba.SE 上的相关答案中演示的那样:

【讨论】:

    【解决方案2】:

    想法是将时间划分为周期,并将它们保存为具有指定粒度的位值。

    • 0 - 一粒粒没有人检查
    • 1 - 有人签到一粒

    假设粒度为 1 小时,周期为 1 天。

    • 000000000000000000000000 表示当天无人签到
    • 000000000000000000000110 表示有人在 21 和 23 之间被检查
    • 000000000000011111000000 表示有人在 13 到 18 点之间被检查
    • 000000000000000111111100 表示有人在 15 到 22 之间被检查

    之后,我们对范围内的每个值进行二元或运算,我们就有了答案。

    • 000000000000011111111110

    它可以在线性时间内完成。这是 Oracle 的一个示例,但可以轻松地将其转换为 PostgreSQL。

    with rec (checkin, checkout)
    as ( select 13, 18 from dual 
       union all 
        select 15, 22 from dual 
       union all 
        select 21, 23 from dual )
    ,spanempty ( empt)
     as ( select '000000000000000000000000' from dual) ,
     spanfull( full)
     as ( select '111111111111111111111111' from dual)
    , bookingbin( binbook) as ( select  substr(empt, 1, checkin) || 
            substr(full, checkin, checkout-checkin) || 
            substr(empt, checkout, 24-checkout) 
     from rec 
     cross join spanempty
     cross join spanfull ),
     bookingInt (rn, intbook) as 
     ( select rownum, bin2dec(binbook) from bookingbin),
     bitAndSum (bitAndSumm) as (
     select sum(bitand(b1.intbook, b2.intbook)) from bookingInt b1 
     join bookingInt b2 
     on b1.rn = b2.rn -1 ) ,
     SumAll (sumall) as (
     select sum(bin2dec(binbook)) from bookingBin  )
    select lpad(dec2bin(sumall - bitAndSumm), 24, '0')
    from SumAll, bitAndSum
    

    结果:

    000000000000011111111110
    

    【讨论】:

      猜你喜欢
      • 2014-06-13
      • 2022-10-15
      • 2016-10-24
      • 1970-01-01
      • 2011-05-27
      • 2014-12-06
      • 1970-01-01
      • 2023-03-26
      • 1970-01-01
      相关资源
      最近更新 更多