在时间戳之间计算工作小时数

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。

1
"09:00 - 17:30(...)午餐时间一小时(...)公司节假日" -- 这是在法国吗? :-) - Denis de Bernardy
2个回答

6

想象一下你有一个工作时间表。(或者自己创建一个。这个没有经过测试,所以可能包含时区和栅木错误。)

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和存储过程和函数都将正常工作。


我删除了我的回答,因为它在Postgres 8.3中无法工作。你说得对。我将代码移到了我针对现代Postgres的相关答案中。 - Erwin Brandstetter
@ErwinBrandstetter:我已经链接到它了。你也许可以使用带有整数参数的generate_series()函数,但这会更加复杂。如果我有时间来处理这个问题,我会在这里添加它。(如果你认为合适的话,也可以在你的答案中添加它。) - Mike Sherrill 'Cat Recall'
你可能会感兴趣:我在另一个答案中进一步推进了你的工作。 - Erwin Brandstetter

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' is not doing what you seem to think. The time zone offset -05 is discarded because you cast the literal to timestamp [without timezone]. You probably wanted timestamptz '2013-01-23 10:47:52-05'. However, working hours are usually bound to local time, so it could be argued that timestamp [without time zone] is a better fit to begin with. More in this related answer:
    Ignoring timezones altogether in Rails and PostgreSQL

  • This form is much more efficient

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

    than this:

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

    You can simply multiply any 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分钟的秒数)来获取(四舍五入的)精确时间单位的数量。
  • 修复包含上边界在内的计数中的误差。

当我尝试创建该函数时,出现以下错误: ERROR: 函数generate_series(integer, double precision)不存在 LINE 8: SELECT $1 + generate_series(0, extract(epoch FROM $2 - $1... ^ HINT: 没有与给定名称和参数类型匹配的函数。您可能需要添加显式类型转换。 - lostinthebits
@lostinthebits:将整数转换的括号放错位置了。我已经修复了,现在应该可以工作了。 - Erwin Brandstetter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接