7

我正在尝试使用 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

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

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

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

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

这是我目前正在处理的查询,目前不起作用(因为我无法FROM在连接的子查询中引用表),但我相信这或多或少是正确的轨道。

这是架构、示例数据和我的工作查询的小提琴。

更新:感谢这个问题,我刚刚发现了一个有趣的事实,即如果您SELECT在查询部分使用集合返回函数,PostgreSQL 将“自动”对集合和行进行交叉连接。我想我已经接近完成这项工作了。

4

3 回答 3

12

首先,你的上边界概念被打破了。时间戳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;

手册中的详细信息
与上述相同的角落案例。

SQL Fiddle演示了所有内容。

于 2013-10-18T19:18:48.250 回答
2

有简单的解决方案(如果间隔在同一时间开始)

postgres=# 选择 i, i + 间隔 '1day' - 间隔 '1sec'
  来自 generate_series('2013-01-01 00:00:00'::timestamp, '2013-01-02 23:59:59', '1day') g(i);
          我│?列?       
──────────────────────┼──────────────────────
 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 行)

我写了一个表函数,它可以在任何时间间隔内完成。速度很快 - 两年范围在 10 毫秒内分为 753 个范围

创建或替换函数 day_ranges(timestamp, timestamp)
以 $$ 形式返回表(t1 时间戳,t2 时间戳)
开始
  t1 := $1;
  如果 $2 > $1 那么
    环形
      如果 t1::date = $2::date 那么
        t2 := $2;
        下一个返回;
        出口;
      万一;
      t2 := date_trunc('day', t1) + 间隔'1day' - 间隔'1sec';
      下一个返回;
      t1 := t2 + 间隔 '1sec';
    结束循环;
  万一;
  返回;
结尾;
$$ 语言 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 行)

时间:6.794 毫秒

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

创建或替换函数 day_ranges(timestamp, timestamp)
以 $$ 形式返回表(t1 时间戳,t2 时间戳)
开始
  t1 := $1; t2 := $2;
  如果 $1::date = $2::date 那么
    下一个返回;
  别的
    -  第一天
    t2 := date_trunc('day', t1) + 间隔'1day' - 间隔'1sec';
    下一个返回;
    如果 $2::date > $1::date + 1 那么
      返回查询选择 d, d + 间隔 '1day' - 间隔 '1sec'
                      从 generate_series(date_trunc('day', $1 + interval '1day')::timestamp,
                                           date_trunc('day', $2 - 间隔'1day')::timestamp,
                                           '1天') g(d);
    万一;
    -  最后一天
    t1 := date_trunc('day', $2); t2 := $2;
    下一个返回;
  万一;
  返回;
结尾;
$$ 语言 plpgsql;
于 2013-10-18T18:26:45.203 回答
1

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

OVERLAPS 行为在文档中解释为:

(start1, end1) OVERLAPS (start2, end2)
(start1, length1) OVERLAPS (start2, length2)
当两个时间段(由它们的端点定义)重叠时,此表达式为真,当它们不重叠时为假。端点可以指定为日期、时间或时间戳对;或作为日期、时间或时间戳,后跟间隔。当提供一对值时,可以先写入开始或结束;OVERLAPS 自动将较早的值作为开始。每个时间段都被认为表示半开区间 start <= time < end,除非 start 和 end 相等,在这种情况下它表示单个时间瞬间。这意味着例如只有一个共同端点的两个时间段不重叠。

使用您的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;

这种技术也可以很容易地添加没有数据的日子,就像日历一样。为此,只需将 替换joinleft join

正如@Erwin在他的回答中指出的那样,etime不应...:59:59将间隔的最后一秒()表示为排除的上限,而应将下一秒表示为排除的上限。您当前的数据可能无关紧要,但如果某些行具有stime...:59:59您碰巧需要亚秒级分辨率,那将是一个问题。

于 2013-10-19T12:16:12.837 回答