在PostgreSQL

时间:2016-01-10 04:16:45

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

我有两个日期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

2 个答案:

答案 0 :(得分: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,或者我们从numeric获取:

     sum(sum(change)) OVER (ORDER BY ts)::int
    
  • sub3中查看上一行的计数
  • 仅在sub4中保留重叠时间范围开始和结束的行,并将时间范围的末尾拉到lead()的同一行。
  • 最后,只保留时间范围开始的行。

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

答案 1 :(得分:1)

想法是将时间划分为句点并将其保存为具有指定粒度的位值。

  • 0 - 没有人检查过一粒
  • 1 - 有人检查了一粒

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

  • 000000000000000000000000表示当天没人接受检查
  • 000000000000000000000110表示有人在21和23之间进行检查
  • 000000000000011111000000表示有人在13到18之间进行检查
  • 000000000000000111111100表示​​有人在15到22之间检查

之后我们对范围中的每个值进行二元OR,我们得到答案。

  • 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