计算PostgreSQL中两个date之间的工作时间

我正在开发一个使用Postgres(PL / pgSQL)的algorithm,我需要计算两个时间戳之间的工作小时数,考虑到周末不工作,剩下的时间只在上午8点到下午15点之间计算。

例子:

  • 从12月3日下午14点到12月4日上午9点应该算两个小时:

    3rd = 1, 4th = 1 
  • 从12月3日下午15点到12月7日上午8点应计算8小时:

     3rd = 0, 4th = 8, 5th = 0, 6th = 0, 7th = 0 

考虑小时数也是很好的。

根据你的问题, 工作时间是: Mo-Fr,08:00-15:00

四舍五入的结果

只有两个给定的时间戳

以1小时为单位进行操作。 分数被忽略,因此不精确但简单:

 SELECT count(*) AS work_hours FROM generate_series (timestamp '2013-06-24 13:30' , timestamp '2013-06-24 15:29' - interval '1h' , interval '1h') h WHERE EXTRACT(ISODOW FROM h) < 6 AND h::time >= '08:00' AND h::time <= '14:00' ; 
  • 函数generate_series()如果结束大于开始,则生成一行,对于每个完整的给定时间间隔(1小时),生成另一行。 这个数字每小时都会计入 。 要忽略小数小时,从最后减去1小时。 而且不要在14:00之前开始计算小时数。

  • EXTRACT()使用字段模式ISODOW而不是DOW来简化expression式。 星期天返回7而不是0

  • 一个简单(而且非常便宜)的time可以很容易地确定合格的时间。

  • 一小时的分数将被忽略,即使分数的开始和结束的时间间隔将加起来一个小时或更多。

整桌子

 CREATE TEMP TABLE t (t_id int PRIMARY KEY, t_start timestamp, t_end timestamp); INSERT INTO t VALUES (1, '2009-12-03 14:00', '2009-12-04 09:00') ,(2, '2009-12-03 15:00', '2009-12-07 08:00') -- examples in question ,(3, '2013-06-24 07:00', '2013-06-24 12:00') ,(4, '2013-06-24 12:00', '2013-06-24 23:00') ,(5, '2013-06-23 13:00', '2013-06-25 11:00') ,(6, '2013-06-23 14:01', '2013-06-24 08:59'); -- max. fractions at begin and end 

查询:

 SELECT t_id, count(*) AS work_hours FROM ( SELECT t_id, generate_series (t_start, t_end - interval '1h', interval '1h') AS h FROM t ) sub WHERE EXTRACT(ISODOW FROM h) < 6 AND h::time >= '08:00' AND h::time <= '14:00' GROUP BY 1 ORDER BY 1; 

SQL小提琴。

更精确

为了获得更高的精度,可以使用更小的时间单位。 5分钟切片例如:

 SELECT t_id, count(*) * interval '5 min' AS work_interval FROM ( SELECT t_id, generate_series (t_start, t_end - interval '5 min', interval '5 min') AS h FROM t ) sub WHERE EXTRACT(ISODOW FROM h) < 6 AND h::time >= '08:00' AND h::time <= '14:55' -- 15.00 - interval '5 min' GROUP BY 1 ORDER BY 1; 

单位越小成本越高

在Postgres 9.3+中用LATERAL清理

结合Postgres 9.3中的新的LATERAL特性,上面的查询可以写成:

1小时精确度:

 SELECT t.t_id, h.work_hours FROM t LEFT JOIN LATERAL ( SELECT count(*) AS work_hours FROM generate_series (t.t_start, t.t_end - interval '1h', interval '1h') h WHERE EXTRACT(ISODOW FROM h) < 6 AND h::time >= '08:00' AND h::time <= '14:00' ) h ON TRUE ORDER BY 1; 

5分钟的精确度:

 SELECT t.t_id, h.work_interval FROM t LEFT JOIN LATERAL ( SELECT count(*) * interval '5 min' AS work_interval FROM generate_series (t.t_start, t.t_end - interval '5 min', interval '5 min') h WHERE EXTRACT(ISODOW FROM h) < 6 AND h::time >= '08:00' AND h::time <= '14:55' ) h ON TRUE ORDER BY 1; 

这还有一个好处 ,就是不会像上面的版本那样排除包含零工作时间的间隔。

更多关于LATERAL

  • 使用group by查找数组中最常用的元素
  • 在另一个表中根据数字在一个表中插入多行

确切的结果

Postgres 8.4+

或者您分别处理时间范围的开始和结束,以获得精确的结果到微秒。 使查询更复杂,但更便宜和准确:

 WITH var AS (SELECT '08:00'::time AS v_start , '15:00'::time AS v_end) SELECT t_id , COALESCE(hh, '0') -- add / subtract fractions - CASE WHEN EXTRACT(ISODOW FROM t_start) < 6 AND t_start::time > v_start AND t_start::time < v_end THEN t_start - date_trunc('hour', t_start) ELSE '0'::interval END + CASE WHEN EXTRACT(ISODOW FROM t_end) < 6 AND t_end::time > v_start AND t_end::time < v_end THEN t_end - date_trunc('hour', t_end) ELSE '0'::interval END AS work_interval FROM t CROSS JOIN var LEFT JOIN ( -- count full hours, similar to above solutions SELECT t_id, count(*)::int * interval '1h' AS h FROM ( SELECT t_id, v_start, v_end , generate_series (date_trunc('hour', t_start) , date_trunc('hour', t_end) - interval '1h' , interval '1h') AS h FROM t, var ) sub WHERE EXTRACT(ISODOW FROM h) < 6 AND h::time >= v_start AND h::time <= v_end - interval '1h' GROUP BY 1 ) h USING (t_id) ORDER BY 1; 

SQL小提琴。

Postgres 9.2+与tsrange

新的范围types提供了一个更加优雅的解决scheme,与交叉路口运营商*

简单的function仅适用于一天的时间范围:

 CREATE OR REPLACE FUNCTION f_worktime_1day(_start timestamp, _end timestamp) RETURNS interval AS $func$ -- _start & _end within one calendar day! - you may want to check ... SELECT CASE WHEN extract(ISODOW from _start) < 6 THEN ( SELECT COALESCE(upper(h) - lower(h), '0') FROM ( SELECT tsrange '[2000-1-1 08:00, 2000-1-1 15:00)' -- hours hard coded * tsrange( '2000-1-1'::date + _start::time , '2000-1-1'::date + _end::time ) AS h ) sub ) ELSE '0' END $func$ LANGUAGE sql IMMUTABLE; 

如果你的范围永远不会跨越多天,这就是你所需要的
否则,使用这个包装函数来处理任何间隔:

 CREATE OR REPLACE FUNCTION f_worktime(_start timestamp , _end timestamp , OUT work_time interval) AS $func$ BEGIN CASE _end::date - _start::date -- spanning how many days? WHEN 0 THEN -- all in one calendar day work_time := f_worktime_1day(_start, _end); WHEN 1 THEN -- wrap around midnight once work_time := f_worktime_1day(_start, NULL) + f_worktime_1day(_end::date, _end); ELSE -- multiple days work_time := f_worktime_1day(_start, NULL) + f_worktime_1day(_end::date, _end) + (SELECT count(*) * interval '7:00' -- workday hard coded! FROM generate_series(_start::date + 1 , _end::date - 1, '1 day') AS t WHERE extract(ISODOW from t) < 6); END CASE; END $func$ LANGUAGE plpgsql IMMUTABLE; 

呼叫:

 SELECT t_id, f_worktime(t_start, t_end) AS worktime FROM t ORDER BY 1; 

SQL小提琴。

怎么样:创build一个24 * 7行的小桌子,每周一小时一行。

 CREATE TABLE hours ( hour timestamp not null, is_working boolean not null ); INSERT INTO hours (hour, is_working) VALUES ('2009-11-2 00:00:00', false), ('2009-11-2 01:00:00', false), . . . ('2009-11-2 08:00:00', true), . . . ('2009-11-2 15:00:00', true), ('2009-11-2 16:00:00', false), . . . ('2009-11-2 23:00:00', false); 

同样地,为其他每一天添加24行。 正如你稍后会看到的那样,无论你给与什么年份或月份。 你只需要代表一周中的所有七天。

 SELECT t.id, t.start, t.end, SUM(CASE WHEN h.is_working THEN 1 ELSE 0 END) AS hours_worked FROM mytable t JOIN hours h ON (EXTRACT(DOW FROM TIMESTAMP h.hour) BETWEEN EXTRACT(DOW FROM TIMESTAMP t.start) AND EXTRACT(DOW FROM TIMESTAMP t.end)) AND (EXTRACT(DOW FROM TIMESTAMP h.hour) > EXTRACT(DOW FROM TIMESTAMP t.start) OR EXTRACT(HOUR FROM TIMESTAMP h.hour) >= EXTRACT(HOUR FROM TIMESTAMP t.start)) AND (EXTRACT(DOW FROM TIMESTAMP h.hour) < EXTRACT(DOW FROM TIMESTAMP t.end) OR EXTRACT(HOUR FROM TIMESTAMP h.hour) <= EXTRACT(HOUR FROM TIMESTAMP t.end)) GROUP BY t.id, t.start, t.end; 

这下面的function将采取的input
每天的工作开始时间
工作结束时间
开始时间
时间结束

 -- helper function CREATE OR REPLACE FUNCTION get_working_time_in_a_day(sdt TIMESTAMP, edt TIMESTAMP, swt TIME, ewt TIME) RETURNS INT AS $$ DECLARE sd TIMESTAMP; ed TIMESTAMP; swdt TIMESTAMP; ewdt TIMESTAMP; seconds INT; BEGIN swdt = sdt::DATE || ' ' || swt; -- work start datetime for a day ewdt = sdt::DATE || ' ' || ewt; -- work end datetime for a day IF (sdt < swdt AND edt <= swdt) -- case 1 and 2 THEN seconds = 0; END IF; IF (sdt < swdt AND edt > swdt AND edt <= ewdt) -- case 3 and 4 THEN seconds = EXTRACT(EPOCH FROM (edt - swdt)); END IF; IF (sdt < swdt AND edt > swdt AND edt > ewdt) -- case 5 THEN seconds = EXTRACT(EPOCH FROM (ewdt - swdt)); END IF; IF (sdt = swdt AND edt > swdt AND edt <= ewdt) -- case 6 and 7 THEN seconds = EXTRACT(EPOCH FROM (edt - sdt)); END IF; IF (sdt = swdt AND edt > ewdt) -- case 8 THEN seconds = EXTRACT(EPOCH FROM (ewdt - sdt)); END IF; IF (sdt > swdt AND edt <= ewdt) -- case 9 and 10 THEN seconds = EXTRACT(EPOCH FROM (edt - sdt)); END IF; IF (sdt > swdt AND sdt < ewdt AND edt > ewdt) -- case 11 THEN seconds = EXTRACT(EPOCH FROM (ewdt - sdt)); END IF; IF (sdt >= ewdt AND edt > ewdt) -- case 12 and 13 THEN seconds = 0; END IF; RETURN seconds; END; $$ LANGUAGE plpgsql; -- Get work time difference CREATE OR REPLACE FUNCTION get_working_time(sdt TIMESTAMP, edt TIMESTAMP, swt TIME, ewt TIME) RETURNS INT AS $$ DECLARE seconds INT = 0; strst VARCHAR(9) = ' 00:00:00'; stret VARCHAR(9) = ' 23:59:59'; tend TIMESTAMP; tempEdt TIMESTAMP; x int; BEGIN <<test>> WHILE sdt <= edt LOOP tend = sdt::DATE || stret; -- get the false end datetime for start time IF edt >= tend THEN tempEdt = tend; ELSE tempEdt = edt; END IF; -- skip saturday and sunday x = EXTRACT(DOW FROM sdt); if (x > 0 AND x < 6) THEN seconds = seconds + get_working_time_in_a_day(sdt, tempEdt, swt, ewt); ELSE -- RAISE NOTICE 'MISSED A DAY'; END IF; sdt = (sdt + (INTERVAL '1 DAY'))::DATE || strst; END LOOP test; --RAISE NOTICE 'diff in minutes = %', (seconds / 60); RETURN seconds; END; $$ LANGUAGE plpgsql; -- Table Definition DROP TABLE IF EXISTS test_working_time; CREATE TABLE test_working_time( pk SERIAL PRIMARY KEY, start_datetime TIMESTAMP, end_datetime TIMESTAMP, start_work_time TIME, end_work_time TIME ); -- Test data insertion INSERT INTO test_working_time VALUES (1, '2015-11-03 01:00:00', '2015-11-03 07:00:00', '08:00:00', '22:00:00'), (2, '2015-11-03 01:00:00', '2015-11-04 07:00:00', '08:00:00', '22:00:00'), (3, '2015-11-03 01:00:00', '2015-11-05 07:00:00', '08:00:00', '22:00:00'), (4, '2015-11-03 01:00:00', '2015-11-06 07:00:00', '08:00:00', '22:00:00'), (5, '2015-11-03 01:00:00', '2015-11-07 07:00:00', '08:00:00', '22:00:00'), (6, '2015-11-03 01:00:00', '2015-11-03 08:00:00', '08:00:00', '22:00:00'), (7, '2015-11-03 01:00:00', '2015-11-04 08:00:00', '08:00:00', '22:00:00'), (8, '2015-11-03 01:00:00', '2015-11-05 08:00:00', '08:00:00', '22:00:00'), (9, '2015-11-03 01:00:00', '2015-11-06 08:00:00', '08:00:00', '22:00:00'), (10, '2015-11-03 01:00:00', '2015-11-07 08:00:00', '08:00:00', '22:00:00'), (11, '2015-11-03 01:00:00', '2015-11-03 11:00:00', '08:00:00', '22:00:00'), (12, '2015-11-03 01:00:00', '2015-11-04 11:00:00', '08:00:00', '22:00:00'), (13, '2015-11-03 01:00:00', '2015-11-05 11:00:00', '08:00:00', '22:00:00'), (14, '2015-11-03 01:00:00', '2015-11-06 11:00:00', '08:00:00', '22:00:00'), (15, '2015-11-03 01:00:00', '2015-11-07 11:00:00', '08:00:00', '22:00:00'), (16, '2015-11-03 01:00:00', '2015-11-03 22:00:00', '08:00:00', '22:00:00'), (17, '2015-11-03 01:00:00', '2015-11-04 22:00:00', '08:00:00', '22:00:00'), (18, '2015-11-03 01:00:00', '2015-11-05 22:00:00', '08:00:00', '22:00:00'), (19, '2015-11-03 01:00:00', '2015-11-06 22:00:00', '08:00:00', '22:00:00'), (20, '2015-11-03 01:00:00', '2015-11-07 22:00:00', '08:00:00', '22:00:00'), (21, '2015-11-03 01:00:00', '2015-11-03 23:00:00', '08:00:00', '22:00:00'), (22, '2015-11-03 01:00:00', '2015-11-04 23:00:00', '08:00:00', '22:00:00'), (23, '2015-11-03 01:00:00', '2015-11-05 23:00:00', '08:00:00', '22:00:00'), (24, '2015-11-03 01:00:00', '2015-11-06 23:00:00', '08:00:00', '22:00:00'), (25, '2015-11-03 01:00:00', '2015-11-07 23:00:00', '08:00:00', '22:00:00'), (26, '2015-11-03 08:00:00', '2015-11-03 11:00:00', '08:00:00', '22:00:00'), (27, '2015-11-03 08:00:00', '2015-11-04 11:00:00', '08:00:00', '22:00:00'), (28, '2015-11-03 08:00:00', '2015-11-05 11:00:00', '08:00:00', '22:00:00'), (29, '2015-11-03 08:00:00', '2015-11-06 11:00:00', '08:00:00', '22:00:00'), (30, '2015-11-03 08:00:00', '2015-11-07 11:00:00', '08:00:00', '22:00:00'), (31, '2015-11-03 08:00:00', '2015-11-03 22:00:00', '08:00:00', '22:00:00'), (32, '2015-11-03 08:00:00', '2015-11-04 22:00:00', '08:00:00', '22:00:00'), (33, '2015-11-03 08:00:00', '2015-11-05 22:00:00', '08:00:00', '22:00:00'), (34, '2015-11-03 08:00:00', '2015-11-06 22:00:00', '08:00:00', '22:00:00'), (35, '2015-11-03 08:00:00', '2015-11-07 22:00:00', '08:00:00', '22:00:00'), (36, '2015-11-03 08:00:00', '2015-11-03 23:00:00', '08:00:00', '22:00:00'), (37, '2015-11-03 08:00:00', '2015-11-04 23:00:00', '08:00:00', '22:00:00'), (38, '2015-11-03 08:00:00', '2015-11-05 23:00:00', '08:00:00', '22:00:00'), (39, '2015-11-03 08:00:00', '2015-11-06 23:00:00', '08:00:00', '22:00:00'), (40, '2015-11-03 08:00:00', '2015-11-07 23:00:00', '08:00:00', '22:00:00'), (41, '2015-11-03 12:00:00', '2015-11-03 18:00:00', '08:00:00', '22:00:00'), (42, '2015-11-03 12:00:00', '2015-11-04 18:00:00', '08:00:00', '22:00:00'), (43, '2015-11-03 12:00:00', '2015-11-05 18:00:00', '08:00:00', '22:00:00'), (44, '2015-11-03 12:00:00', '2015-11-06 18:00:00', '08:00:00', '22:00:00'), (45, '2015-11-03 12:00:00', '2015-11-07 18:00:00', '08:00:00', '22:00:00'), (46, '2015-11-03 12:00:00', '2015-11-03 22:00:00', '08:00:00', '22:00:00'), (47, '2015-11-03 12:00:00', '2015-11-04 22:00:00', '08:00:00', '22:00:00'), (48, '2015-11-03 12:00:00', '2015-11-05 22:00:00', '08:00:00', '22:00:00'), (49, '2015-11-03 12:00:00', '2015-11-06 22:00:00', '08:00:00', '22:00:00'), (50, '2015-11-03 12:00:00', '2015-11-07 22:00:00', '08:00:00', '22:00:00'), (51, '2015-11-03 12:00:00', '2015-11-03 23:00:00', '08:00:00', '22:00:00'), (52, '2015-11-03 12:00:00', '2015-11-04 23:00:00', '08:00:00', '22:00:00'), (53, '2015-11-03 12:00:00', '2015-11-05 23:00:00', '08:00:00', '22:00:00'), (54, '2015-11-03 12:00:00', '2015-11-06 23:00:00', '08:00:00', '22:00:00'), (55, '2015-11-03 12:00:00', '2015-11-07 23:00:00', '08:00:00', '22:00:00'), (56, '2015-11-03 22:00:00', '2015-11-03 23:00:00', '08:00:00', '22:00:00'), (57, '2015-11-03 22:00:00', '2015-11-04 23:00:00', '08:00:00', '22:00:00'), (58, '2015-11-03 22:00:00', '2015-11-05 23:00:00', '08:00:00', '22:00:00'), (59, '2015-11-03 22:00:00', '2015-11-06 23:00:00', '08:00:00', '22:00:00'), (60, '2015-11-03 22:00:00', '2015-11-07 23:00:00', '08:00:00', '22:00:00'), (61, '2015-11-03 22:30:00', '2015-11-03 23:30:00', '08:00:00', '22:00:00'), (62, '2015-11-03 22:30:00', '2015-11-04 23:30:00', '08:00:00', '22:00:00'), (63, '2015-11-03 22:30:00', '2015-11-05 23:30:00', '08:00:00', '22:00:00'), (64, '2015-11-03 22:30:00', '2015-11-06 23:30:00', '08:00:00', '22:00:00'), (65, '2015-11-03 22:30:00', '2015-11-07 23:30:00', '08:00:00', '22:00:00'); -- select query to get work time difference SELECT start_datetime, end_datetime, start_work_time, end_work_time, get_working_time(start_datetime, end_datetime, start_work_time, end_work_time) AS diff_in_minutes FROM test_working_time; 

这将只给出在开始和结束date之间的工作时间(秒)的差异