3

我正在使用 Postgres 8.3(目前版本中没有选择)。我的原始数据表如下:

ID  start_time               finish_time
01   2013-01-23 10:47:52-05  2013-02-25 11:18:36-05

我可以在两个时间戳之间进行计数:

--relevant line in view creation query:
date_part('epoch',(finish_time - start_time)::interval)/3600 as hours

我不想包括周末。另外,我只想数 09:00 - 17:30。

在一个完美的世界中,我也会每天减少一个小时的午餐时间,最终我还想包括公司假期,但我只想先解决这个工作时间部分。

关于如何解决这个问题的任何建议?我对 SQL 很陌生。我也愿意使用 SQLalchemy,但我也是那里的初学者,对直接 SQL 感觉更舒服。

4

2 回答 2

5

想象一下,您有一张工作分钟表。(或者构建一个。这个没有经过测试,所以它可能包含时区和栅栏错误。)

create table work_minutes (
  work_minute timestamp primary key
);

insert into work_minutes
select work_minute
from 
  (select generate_series(timestamp '2013-01-01 00:00:00', timestamp '2013-12-31 11:59:00', '1 minute') as work_minute) t
where extract(isodow from work_minute) < 6
  and cast(work_minute as time) between time '09:00' and time '17:30'

现在您的查询可以计算分钟数,这非常简单。

select count(*)/60.0 as elapsed_hrs
from work_minutes
where work_minute between '2013-01-23 10:47:52' and '2013-02-25 11:18:36'

elapsed_hours
--
196.4

您可以决定如何处理小数小时。

按分钟计算和按小时计算之间可能存在很大差异,具体取决于您如何处理开始时间等。基于小时的计算可能不会计算超过停止时间的一小时内的很多分钟。是否重要取决于应用程序。

您可以使用 generate_series() 即时生成这样的虚拟表,但是这样的基表只需要大约 400 万行即可覆盖 30 年,而且这种计算速度非常快。

之后 。. .

我看到Erwin Brandstetter 介绍了现代 PostgreSQL 中 generate_series() 的使用;它不适用于 8.3 版本,因为 8.3 不支持公用表表达式或 generate_series(timestamp, timestamp)。这是避免这些问题的 Erwin 查询的一个版本。这不是一个完全忠实的翻译;计算相差一个小时。这可能是我的一个栅栏错误,但我现在没有时间深入研究细节。

select count(*) from 
(select timestamp '2013-01-23 10:47:52-05' + (n || ' hours')::interval
from generate_series(  0
                     , (extract(days from timestamp '2013-02-25 11:18:36-05' 
                                        - timestamp '2013-01-23 10:47:52-05')::integer * 24) ) n
where extract(isodow from (timestamp '2013-01-23 10:47:52-05' + (n || ' hours')::interval)) < 6
  and (timestamp '2013-01-23 10:47:52-05' + (n || ' hours')::interval)::time >= '09:00'::time
  and (timestamp '2013-01-23 10:47:52-05' + (n || ' hours')::interval)::time <  '17:30'::time
 ) t

基于表格的解决方案具有轻松处理管理奇思妙想的优势。“喂!我们家狗生了七只小狗!今天半天!” 它也可以很好地扩展,并且几乎可以在每个平台上运行而无需修改。

如果您使用 generate_series(),请将其包装在视图中。这样,可以在一个地方管理对规则的任意更改。如果规则变得过于复杂而无法在视图中轻松维护,您可以将视图替换为具有相同名称的表,所有应用程序代码、SQL 以及存储过程和函数都将正常工作。

于 2013-06-25T13:42:35.337 回答
3

这推进了@Catcall 提供的正在进行的工作

SELECT count(*)
FROM   generate_series(0, extract(days from timestamp '2013-02-25 11:18:36' 
                                          - timestamp '2013-01-23 10:47:52')::int * 24) n
WHERE  extract(ISODOW from timestamp '2013-01-23 10:47:52' + n * interval '1h') < 6
AND   (timestamp '2013-01-23 10:47:52' + n * interval '1h')::time >= '09:00'::time
AND   (timestamp '2013-01-23 10:47:52' + n * interval '1h')::time <  '17:30'::time
  • timestamp '2013-01-23 10:47:52-05'没有按照你的想法去做。时区偏移量-05被丢弃,因为您将文字转换为timestamp [without timezone]. 你可能想要timestamptz '2013-01-23 10:47:52-05'. 但是,工作时间通常与当地时间有关,因此可以说这timestamp [without time zone]是更合适的开始。此相关答案中的更多内容:
    在 Rails 和 PostgreSQL 中完全忽略时区

  • 这种形式效率更高

    timestamptz '2013-01-23 10:47:52-05' + n * interval '1h'
    

    比这个:

    timestamptz '2013-01-23 10:47:52-05' + (n || ' hours')::interval
    

    您可以简单地乘以任何间隔。

功能

我进一步开发并将其包装成一个 SQL 函数。
仍然不精确,但它修复了系统误差,并且由于半小时单位而具有较小的舍入误差。

CREATE OR REPLACE FUNCTION f_worktime83(t_start timestamp
                                      , t_end timestamp)
  RETURNS interval AS
$func$

SELECT (count(*) - 1) * interval '30 min' -- fix off-by-one error
FROM   (
   SELECT $1 + generate_series(0, (extract(epoch FROM $2 - $1)/1800)::int)
             * interval '30 min' AS t
   ) sub
WHERE  extract(ISODOW from t) < 6
AND    t::time >= '09:00'::time
AND    t::time <  '17:30'::time

$func$  LANGUAGE sql

称呼:

SELECT f_worktime83('2013-06-26 10:47:52', '2013-06-26 11:10:51')
  • 直接加值generate_series(),简化代码。
  • epoc通过提取(秒数)并将其除以1800(30 分钟内的秒数)来获得(四舍五入)精确的时间单位数。
  • 修复计数中包含上边界的 off-by-1 错误。
于 2013-06-26T02:42:54.373 回答