【问题标题】:PostgreSQL splitting time range into daysPostgreSQL 将时间范围拆分为天
【发布时间】:2013-10-18 16:49:44
【问题描述】:

我正在尝试使用 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 语句是Oracle 对SQL 标准的扩展,所以我不能使用它。

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

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

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

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

【问题讨论】:

  • 不言而喻,您的 Postgres 版本对这个问题至关重要。怎么能担心代码“非常陈旧”而仍然不让我们知道这些信息?
  • 你说得对,这是一个愚蠢的遗漏。我正在使用 PostgreSQL 9.2.4。
  • 最好把它放在你的问题中,而不仅仅是评论......在这样做的同时,还要添加你现在拥有的查询,即使它不起作用......
  • 关于目标列表中的 SRF 函数(“自动”进行交叉连接):这是一个非常强大的功能,但会产生令人讨厌的副作用,并且完全非 SQL 标准。 Consider this related answer and the comments and links for more information!它的要点:一旦您拥有 Postgres 9.3,请改用 LATERAL - 正如我在下面的回答中所详述的那样。

标签: sql postgresql date-range generate-series


【解决方案1】:

首先,您的上边界概念被打破。带有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 演示全部。

【讨论】:

  • 这是一个非常棒的答案。它不仅解决了我遇到的问题,还解决了我什至不知道的一两个问题。谢谢! :)
  • @CmdrMoozy:有一个值得注意的极端情况。我为此添加了一个修复程序。另外,我会对查询与Pavel's function 的比较感兴趣。如果您碰巧对两者都进行了测试,您可能会在结果中留下备注。
  • 一个小更正:在您帖子的第三个查询中,我认为generate_series(stime, etime, interval '1d')::date 应该是generate_series(stime::date, etime::date, interval '1d'),以处理stimeetime 落在不同日期的情况,但是相隔不到一天。
  • @CmdrMoozy:是的,这应该使它完整。应用了你的修复。
【解决方案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;

【讨论】:

  • 在我的例子中,“from”和“to”时间是任意的,尽管我有一个保证from &lt; to的约束。另一个问题是我需要选择除fromto 之外的一些其他列,为每个“拆分”结果行复制其他列的值。
  • @CmdrMoozy:很明显,您还应该提供示例数据,可能还应该提供表定义。最好的 SQLfiddle。随机示例:sqlfiddle.com/#!12/1cb5c/8
【解决方案3】:

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

OVERLAPS 行为在 doc 中解释如下:

(start1, end1) OVERLAPS (start2, end2)
(start1, length1) OVERLAPS (start2, length2)
当两个时间段时,此表达式为真 (由它们的端点定义)重叠,当它们不重叠时为假。 端点可以指定为日期、时间或时间对 邮票;或作为日期、时间或时间戳,后跟间隔。 当提供一对值时,可以是开始或结束 先写; OVERLAPS 自动采用较早的值 配对作为开始。每个时间段都被认为代表 半开区间 start

使用您的 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),而应将下一秒作为排除的上限。这可能与您当前的数据无关,但如果某些行在 ...:59:59 处有 stime,或者如果您碰巧需要亚秒级分辨率,那将是一个问题。

【讨论】:

    猜你喜欢
    • 2012-10-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-17
    • 2021-12-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多