我正在尝试使用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将“自动”在集合和行上进行交叉连接。我想我已经接近完成这项工作了。
答案 0 :(得分:7)
首先,您的上边框概念已损坏。 23:59:59
的时间戳不好。数据类型timestamp
包含小数位数。那么2013-10-18 23:59:59.123::timestamp
呢?
包含下边框,排除逻辑中无处不在的上边框。比较:
在此前提的基础上:
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;
更简单的一个甚至可能更快
当stime
和etime
完全属于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+中,您最好使用 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
或者您碰巧需要亚秒级分辨率,那么这将是一个问题。