Theo câu hỏi của bạn giờ làm việc là: Mo – Fr, 08:00–15:00 .
Kết quả làm tròn
Chỉ cho hai dấu thời gian nhất định
Hoạt động trên đơn vị 1 giờ . Các phân số bị bỏ qua, do đó không chính xác nhưng đơn giản:
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';
-
Hàm
generate_series()
tạo một hàng nếu phần cuối lớn hơn phần đầu và một hàng khác cho mọi đầy đủ khoảng thời gian nhất định (1 giờ). Số lượng lớn này mỗi giờ được nhập vào . Để bỏ qua số giờ phân số, hãy trừ đi 1 giờ ở cuối. Và không tính số giờ bắt đầu trước 14:00. -
Sử dụng mẫu trường
ISODOW
thay vìDOW
choEXTRACT()
để đơn giản hóa các biểu thức. Trả về7
thay vì0
cho các ngày Chủ nhật. -
Truyền đơn giản (và rất rẻ) đến
time
giúp dễ dàng xác định giờ đủ điều kiện. -
Các phân số của một giờ bị bỏ qua, ngay cả khi các phân số ở đầu và cuối của khoảng thời gian đó cộng lại lên đến một giờ hoặc hơn.
Đối với toàn bộ bảng
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
Truy vấn:
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 Fiddle.
Chính xác hơn
Để có độ chính xác cao hơn, bạn có thể sử dụng các đơn vị thời gian nhỏ hơn. Ví dụ:lát cắt 5 phút:
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;
Đơn vị càng nhỏ thì chi phí càng cao .
Dọn dẹp với LATERAL
trong Postgres 9.3+
Kết hợp với LATERAL
mới trong Postgres 9.3, truy vấn trên có thể được viết thành:
Độ chính xác 1 giờ:
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;
Độ chính xác 5 phút:
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;
Điều này có lợi thế bổ sung rằng các khoảng thời gian không chứa giờ làm việc không bị loại trừ khỏi kết quả như trong các phiên bản trên.
Tìm hiểu thêm về LATERAL
:
- Tìm các phần tử phổ biến nhất trong mảng với một nhóm bằng
- Chèn nhiều hàng vào một bảng dựa trên số trong một bảng khác
Kết quả chính xác
Postgres 8.4+
Hoặc bạn giải quyết riêng khung thời gian bắt đầu và kết thúc để có được chính xác kết quả đến micro giây. Làm cho truy vấn phức tạp hơn, nhưng rẻ hơn và chính xác hơn:
WITH var AS (SELECT '08:00'::time AS v_start
, '15:00'::time AS v_end)
SELECT t_id
, COALESCE(h.h, '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 Fiddle.
Postgres 9.2+ với tsrange
Các loại phạm vi mới cung cấp giải pháp thanh lịch hơn để có kết quả chính xác kết hợp với toán tử giao lộ *
:
Hàm đơn giản cho các phạm vi thời gian chỉ kéo dài một ngày:
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;
Nếu phạm vi của bạn không bao giờ kéo dài nhiều ngày, đó là tất cả những gì bạn cần .
Ngoài ra, hãy sử dụng chức năng trình bao bọc này để xử lý bất kỳ khoảng thời gian:
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;
Gọi:
SELECT t_id, f_worktime(t_start, t_end) AS worktime
FROM t
ORDER BY 1;
SQL Fiddle.