PostgreSQL将时间范围分成几天

时间:2013-10-18 16:49:44

标签: sql postgresql date-range generate-series

我正在尝试使用PostgreSQL 9.2.4编写一个复杂的查询,但我无法正常工作。我有一个包含时间范围的表,以及其他几个列。当我在此表中存储数据时,如果所有列都相同且时间范围重叠或相邻,我将它们组合成一行。

但是当我检索它们时,我想在天边界处分割范围 - 例如:

2013-01-01 00:00:00 to 2013-01-02 23:59:59

将被选为两行:

2013-01-01 00:00:00 to 2013-01-01 23:59:59
2013-01-02 00:00:00 to 2013-01-02 23:59:59

,其他列中的值对于两个检索到的条目都相同。

我已经看到this question似乎或多或少地解决了我想要的问题,但它适用于PostgreSQL的“非常旧”版本,所以我不确定它是否真的适用。

我也看过this question,它完全符合我的要求,但据我所知CONNECT BY语句是SQL标准的Oracle扩展,所以我不能使用它

我相信我可以使用PostgreSQL的generate_series实现这一目标,但我希望有一个简单的例子可以证明它是如何用来做到这一点的。

这是我目前正在处理的查询,目前无效(因为我无法在连接的子查询中引用FROM表),但我相信这更多 - 或者 - 没有正确的轨道。

Here's the fiddle包含架构,示例数据和我的工作查询。

更新:我发现了一个有趣的事实,感谢this question,如果你在查询的SELECT部分使用set-returning函数,PostgreSQL将“自动”在集合和行上进行交叉连接。我想我已经接近完成这项工作了。

3 个答案:

答案 0 :(得分:7)

首先,您的上边框概念已损坏23:59:59的时间戳不好。数据类型timestamp包含小数位数。那么2013-10-18 23:59:59.123::timestamp呢?

包含下边框,排除逻辑中无处不在的上边框。比较:

在此前提的基础上:

Postgres 9.2或更早

SELECT id
     , stime
     , etime
FROM   timesheet_entries t
WHERE  etime <= stime::date + 1  -- this includes upper border 00:00

UNION ALL
SELECT id
     , CASE WHEN stime::date = d THEN stime ELSE d END     -- AS stime
     , CASE WHEN etime::date = d THEN etime ELSE d + 1 END -- AS etime
FROM (
   SELECT id
        , stime
        , etime
        , generate_series(stime::date, etime::date, interval '1d')::date AS d
   FROM   timesheet_entries t
   WHERE  etime > stime::date + 1
   ) sub
ORDER  BY id, stime;

或者简单地说:

SELECT id
     , CASE WHEN stime::date = d THEN stime ELSE d END     -- AS stime
     , CASE WHEN etime::date = d THEN etime ELSE d + 1 END -- AS etime
FROM (
   SELECT id
        , stime
        , etime
        , generate_series(stime::date, etime::date, interval '1d')::date AS d
   FROM   timesheet_entries t
   ) sub
ORDER  BY id, stime;

更简单的一个甚至可能更快 当stimeetime完全属于00:00时,请注意极限案例差异。然后在末尾添加具有零时间范围的行。有各种方法可以解决这个问题。我建议:

SELECT *
FROM  (
   SELECT id
        , CASE WHEN stime::date = d THEN stime ELSE d END     AS stime
        , CASE WHEN etime::date = d THEN etime ELSE d + 1 END AS etime
   FROM (
      SELECT id
           , stime
           , etime
           , generate_series(stime::date, etime::date, interval '1d')::date AS d
      FROM   timesheet_entries t
      ) sub1
   ORDER  BY id, stime
   ) sub2
WHERE  etime <> stime;

Postgres 9.3 +

在Postgres 9.3+中,您最好使用 LATERAL

SELECT id
     , CASE WHEN stime::date = d THEN stime ELSE d END     AS stime
     , CASE WHEN etime::date = d THEN etime ELSE d + 1 END AS etime
FROM   timesheet_entries t
     , LATERAL (SELECT d::date
                FROM   generate_series(t.stime::date, t.etime::date, interval '1d') d
                ) d
ORDER  BY id, stime;

Details in the manual
与上述相同的角落情况。

SQL Fiddle展示所有。

答案 1 :(得分:2)

只有解决方案(如果间隔同时开始)

postgres=# select i, i + interval '1day' - interval '1sec' 
  from generate_series('2013-01-01 00:00:00'::timestamp, '2013-01-02 23:59:59', '1day') g(i);
          i          │      ?column?       
─────────────────────┼─────────────────────
 2013-01-01 00:00:00 │ 2013-01-01 23:59:59
 2013-01-02 00:00:00 │ 2013-01-02 23:59:59
(2 rows)

我写了一个表函数,它可以用于任何间隔。它很快 - 两年的范围分为753个范围,10ms

create or replace function day_ranges(timestamp, timestamp)
returns table(t1 timestamp, t2 timestamp) as $$
begin
  t1 := $1;
  if $2 > $1 then
    loop
      if t1::date = $2::date then
        t2 := $2;
        return next;
        exit;
      end if;
      t2 := date_trunc('day', t1) + interval '1day' - interval '1sec';
      return next;
      t1 := t2 + interval '1sec';
    end loop;
  end if;
  return;
end;
$$ language plpgsql;

结果:

postgres=# select * from day_ranges('2013-10-08 22:00:00', '2013-10-10 23:00:00');
         t1          │         t2          
─────────────────────┼─────────────────────
 2013-10-08 22:00:00 │ 2013-10-09 23:59:59
 2013-10-09 00:00:00 │ 2013-10-09 23:59:59
 2013-10-10 00:00:00 │ 2013-10-10 23:00:00
(3 rows)

Time: 6.794 ms

基于RETURN QUERY的更快(和更长一点)的版本

create or replace function day_ranges(timestamp, timestamp)
returns table(t1 timestamp, t2 timestamp) as $$
begin
  t1 := $1; t2 := $2;
  if $1::date = $2::date then
    return next;
  else
    -- first day
    t2 := date_trunc('day', t1) + interval '1day' - interval '1sec';
    return next;
    if $2::date > $1::date + 1 then
      return query select d, d + interval '1day' - interval '1sec'
                      from generate_series(date_trunc('day', $1 + interval '1day')::timestamp,
                                           date_trunc('day', $2 - interval '1day')::timestamp,
                                           '1day') g(d);
    end if;
    -- last day 
    t1 := date_trunc('day', $2); t2 := $2;
    return next;
  end if;
  return;
end;
$$ language plpgsql;

答案 2 :(得分:1)

您可以通过简单地生成整个范围集中的所有日期来构建结果,并使用OVERLAPS运算符将其连接到范围本身。这将丢弃没有数据的日期,并在数据日期内执行所需的笛卡尔积。

OVERLAPS行为在doc中解释为:

  

(start1,end1)OVERLAPS(start2,end2)
  (start1,length1)OVERLAPS(start2,length2)
  当两个时间段时,此表达式产生true   (由它们的端点定义)重叠,当它们不重叠时为假。   端点可以指定为日期,时间或时间对   邮票;或作为日期,时间或时间戳,后跟一个间隔。   当提供一对值时,可以是开始值或结束值   先写的; OVERLAPS自动获取之前的值   配对作为开始。每个时间段都被认为代表了   半开间隔开始&lt; =时间&lt;结束,除非开始和结束是平等的   在这种情况下,它代表单一时刻。这意味着   例如,只有一个共同点的两个时间段没有   重叠。

使用您的timesheet_entries表格,查询将为:

select days.day, timesheet_entries.* from
  (select day from generate_series(
         (select min(stime) from timesheet_entries),
         (select max(etime) from timesheet_entries),
         '1 day'::interval) day
  ) days
 join timesheet_entries 
  on (stime,etime) overlaps (days.day,days.day+'1 day'::interval)
 order by 1;

这种技术可以轻松添加没有数据的日期,类似日历。为此,只需将join替换为left join

正如@Erwin in his answer指出的那样,etime不应代表间隔的最后一秒(...:59:59),而应代表下一秒作为排除的上限。对于您当前的数据,这可能无关紧要,但如果某些行在stime处有...:59:59或者您碰巧需要亚秒级分辨率,那么这将是一个问题。