PostgreSQL
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> PostgreSQL

Tính giờ làm việc giữa 2 ngày trong PostgreSQL

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 cho EXTRACT() để đơ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.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. PostgreSQL -phải xuất hiện trong mệnh đề GROUP BY hoặc được sử dụng trong một hàm tổng hợp

  2. Sử dụng bản sao lôgic PostgreSQL để duy trì máy chủ kiểm tra đọc / ghi luôn cập nhật

  3. lỗi lệnh sử dụng alembic không thể tìm thấy mã định danh

  4. Ứng dụng Spring Boot bị kẹt trên Hikari-Pool-1 - Đang khởi động ...

  5. Postgres:lệnh chân không không làm sạch các bộ giá trị đã chết